From 7328229bd841eb833e07e2f814642d4739295414 Mon Sep 17 00:00:00 2001 From: hangillee Date: Fri, 12 Jul 2024 11:03:51 +0900 Subject: [PATCH 001/108] =?UTF-8?q?feat:=20Swagger=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 3 +++ .../touroot/global/config/SwaggerConfig.java | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java diff --git a/backend/build.gradle b/backend/build.gradle index 6efc0c7f..a8d8e817 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -27,10 +27,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java new file mode 100644 index 00000000..3870939a --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package woowacourse.touroot.global.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI createOpenApi() { + return new OpenAPI() + .info(getInfo()); + } + + private Info getInfo() { + return new Info() + .title("touroot API") + .description("To your route, 투룻 API") + .version("0.1"); + } +} From 3f8cc6c5aeeb0531745b41ddf763f5e974fab71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=89=E1=85=A9=E1=86=AB=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=A7=E1=86=BC?= Date: Fri, 12 Jul 2024 10:46:43 +0900 Subject: [PATCH 002/108] =?UTF-8?q?docs:=20pull=20request=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..e413ffe6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +# ✅ 작업 내용 + +- 작업 내용 1 +- 작업 내용 2 +- 작업 내용 3 + +# 📸 스크린샷 + +# 🙈 참고 사항 From c3c2e589f2a54e8eb80134bcd3491d7b9485907c Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Mon, 15 Jul 2024 17:26:05 +0900 Subject: [PATCH 003/108] =?UTF-8?q?[Feature]=20-=20DockerFile=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Dockerfile 작성 * chore: EOL 추가 --- backend/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 backend/Dockerfile diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..6958813d --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:17-oracle +ARG JAR_FILE=build/libs/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app.jar"] From 97fdee6dfda4d1017047eb84f40a4b9c700736f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Mon, 15 Jul 2024 19:18:39 +0900 Subject: [PATCH 004/108] =?UTF-8?q?feat:=20CI=20workflow=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/be-dev-ci.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/be-dev-ci.yml diff --git a/.github/workflows/be-dev-ci.yml b/.github/workflows/be-dev-ci.yml new file mode 100644 index 00000000..e64bb801 --- /dev/null +++ b/.github/workflows/be-dev-ci.yml @@ -0,0 +1,31 @@ +name: BE-DEV-CI + +on: + push: + branches: [ develop/be ] + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew clean build + + - name: Build the Docker image + run: docker build -f ./backend/Dockerfile --no-cache -t touroot/touroot-api . From 1ea470af1ff84caa8260950a97e3d4199a5e3a8e Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 15 Jul 2024 20:01:50 +0900 Subject: [PATCH 005/108] =?UTF-8?q?fix:=20gradle=20wrapper=20=EC=9E=AC?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43504 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- backend/gradlew | 5 +- backend/gradlew.bat | 94 ++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) mode change 100644 => 100755 backend/gradlew diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch literal 43504 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vW>HF-ViB*%t0;Thq2} z+qP}n=Cp0wwr%5S+qN<7?r+``=l(h0z2`^8j;g2~Q4u?{cIL{JYY%l|iw&YH4FL(8 z1-*E#ANDHi+1f%lMJbRfq*`nG)*#?EJEVoDH5XdfqwR-C{zmbQoh?E zhW!|TvYv~>R*OAnyZf@gC+=%}6N90yU@E;0b_OV#xL9B?GX(D&7BkujjFC@HVKFci zb_>I5e!yuHA1LC`xm&;wnn|3ht3h7|rDaOsh0ePhcg_^Wh8Bq|AGe`4t5Gk(9^F;M z8mFr{uCm{)Uq0Xa$Fw6+da`C4%)M_#jaX$xj;}&Lzc8wTc%r!Y#1akd|6FMf(a4I6 z`cQqS_{rm0iLnhMG~CfDZc96G3O=Tihnv8g;*w?)C4N4LE0m#H1?-P=4{KeC+o}8b zZX)x#(zEysFm$v9W8-4lkW%VJIjM~iQIVW)A*RCO{Oe_L;rQ3BmF*bhWa}!=wcu@# zaRWW{&7~V-e_$s)j!lJsa-J?z;54!;KnU3vuhp~(9KRU2GKYfPj{qA?;#}H5f$Wv-_ zGrTb(EAnpR0*pKft3a}6$npzzq{}ApC&=C&9KoM3Ge@24D^8ZWJDiXq@r{hP=-02& z@Qrn-cbr2YFc$7XR0j7{jAyR;4LLBf_XNSrmd{dV3;ae;fsEjds*2DZ&@#e)Qcc}w zLgkfW=9Kz|eeM$E`-+=jQSt}*kAwbMBn7AZSAjkHUn4n||NBq*|2QPcKaceA6m)g5 z_}3?DX>90X|35eI7?n+>f9+hl5b>#q`2+`FXbOu9Q94UX-GWH;d*dpmSFd~7WM#H2 zvKNxjOtC)U_tx*0(J)eAI8xAD8SvhZ+VRUA?)| zeJjvg9)vi`Qx;;1QP!c_6hJp1=J=*%!>ug}%O!CoSh-D_6LK0JyiY}rOaqSeja&jb#P|DR7 z_JannlfrFeaE$irfrRIiN|huXmQhQUN6VG*6`bzN4Z3!*G?FjN8!`ZTn6Wn4n=Ync z_|Sq=pO7+~{W2}599SfKz@umgRYj6LR9u0*BaHqdEw^i)dKo5HomT9zzB$I6w$r?6 zs2gu*wNOAMK`+5yPBIxSOJpL$@SN&iUaM zQ3%$EQt%zQBNd`+rl9R~utRDAH%7XP@2Z1s=)ks77I(>#FuwydE5>LzFx)8ye4ClM zb*e2i*E$Te%hTKh7`&rQXz;gvm4Dam(r-!FBEcw*b$U%Wo9DIPOwlC5Ywm3WRCM4{ zF42rnEbBzUP>o>MA){;KANhAW7=FKR=DKK&S1AqSxyP;k z;fp_GVuV}y6YqAd)5p=tJ~0KtaeRQv^nvO?*hZEK-qA;vuIo!}Xgec4QGW2ipf2HK z&G&ppF*1aC`C!FR9(j4&r|SHy74IiDky~3Ab)z@9r&vF+Bapx<{u~gb2?*J zSl{6YcZ$&m*X)X?|8<2S}WDrWN3yhyY7wlf*q`n^z3LT4T$@$y``b{m953kfBBPpQ7hT;zs(Nme`Qw@{_pUO0OG zfugi3N?l|jn-Du3Qn{Aa2#6w&qT+oof=YM!Zq~Xi`vlg<;^)Jreeb^x6_4HL-j}sU z1U^^;-WetwPLKMsdx4QZ$haq3)rA#ATpEh{NXto-tOXjCwO~nJ(Z9F%plZ{z(ZW!e zF>nv&4ViOTs58M+f+sGimF^9cB*9b(gAizwyu5|--SLmBOP-uftqVnVBd$f7YrkJ8!jm*QQEQC zEQ+@T*AA1kV@SPF6H5sT%^$$6!e5;#N((^=OA5t}bqIdqf`PiMMFEDhnV#AQWSfLp zX=|ZEsbLt8Sk&wegQU0&kMC|cuY`&@<#r{t2*sq2$%epiTVpJxWm#OPC^wo_4p++U zU|%XFYs+ZCS4JHSRaVET)jV?lbYAd4ouXx0Ka6*wIFBRgvBgmg$kTNQEvs0=2s^sU z_909)3`Ut!m}}@sv<63E@aQx}-!qVdOjSOnAXTh~MKvr$0nr(1Fj-3uS{U6-T9NG1Y(Ua)Nc}Mi< zOBQz^&^v*$BqmTIO^;r@kpaq3n!BI?L{#bw)pdFV&M?D0HKqC*YBxa;QD_4(RlawI z5wBK;7T^4dT7zt%%P<*-M~m?Et;S^tdNgQSn?4$mFvIHHL!`-@K~_Ar4vBnhy{xuy zigp!>UAwPyl!@~(bkOY;un&B~Evy@5#Y&cEmzGm+)L~4o4~|g0uu&9bh8N0`&{B2b zDj2>biRE1`iw}lv!rl$Smn(4Ob>j<{4dT^TfLe-`cm#S!w_9f;U)@aXWSU4}90LuR zVcbw;`2|6ra88#Cjf#u62xq?J)}I)_y{`@hzES(@mX~}cPWI8}SRoH-H;o~`>JWU$ zhLudK3ug%iS=xjv9tnmOdTXcq_?&o30O;(+VmC&p+%+pd_`V}RY4ibQMNE&N5O+hb3bQ8bxk^33Fu4DB2*~t1909gqoutQHx^plq~;@g$d_+rzS0`2;}2UR2h#?p35B=B*f0BZS4ysiWC!kw?4B-dM%m6_BfRbey1Wh? zT1!@>-y=U}^fxH0A`u1)Mz90G6-<4aW^a@l_9L6Y;cd$3<#xIrhup)XLkFi$W&Ohu z8_j~-VeVXDf9b&6aGelt$g*BzEHgzh)KDgII_Y zb$fcY8?XI6-GEGTZVWW%O;njZld)29a_&1QvNYJ@OpFrUH{er@mnh*}326TYAK7_Z zA={KnK_o3QLk|%m@bx3U#^tCChLxjPxMesOc5D4G+&mvp@Clicz^=kQlWp1|+z|V7 zkU#7l61m@^#`1`{+m2L{sZC#j?#>0)2z4}}kqGhB{NX%~+3{5jOyij!e$5-OAs zDvq+>I2(XsY9%NNhNvKiF<%!6t^7&k{L7~FLdkP9!h%=2Kt$bUt(Zwp*&xq_+nco5 zK#5RCM_@b4WBK*~$CsWj!N!3sF>ijS=~$}_iw@vbKaSp5Jfg89?peR@51M5}xwcHW z(@1TK_kq$c4lmyb=aX3-JORe+JmuNkPP=bM*B?};c=_;h2gT-nt#qbriPkpaqoF@q z<)!80iKvTu`T-B3VT%qKO^lfPQ#m5Ei6Y%Fs@%Pt!8yX&C#tL$=|Ma8i?*^9;}Fk> zyzdQQC5YTBO&gx6kB~yhUUT&%q3a3o+zueh>5D7tdByYVcMz@>j!C@Iyg{N1)veYl`SPshuH6Rk=O6pvVrI71rI5*%uU3u81DpD%qmXsbKWMFR@2m4vO_^l6MMbO9a()DcWmYT&?0B_ zuY~tDiQ6*X7;9B*5pj?;xy_B}*{G}LjW*qU&%*QAyt30@-@O&NQTARZ+%VScr>`s^KX;M!p; z?8)|}P}L_CbOn!u(A{c5?g{s31Kn#7i)U@+_KNU-ZyVD$H7rtOjSht8%N(ST-)%r` z63;Hyp^KIm-?D;E-EnpAAWgz2#z{fawTx_;MR7)O6X~*jm*VUkam7>ueT^@+Gb3-Y zN3@wZls8ibbpaoR2xH=$b3x1Ng5Tai=LT2@_P&4JuBQ!r#Py3ew!ZVH4~T!^TcdyC ze#^@k4a(nNe~G+y zI~yXK@1HHWU4pj{gWT6v@$c(x){cLq*KlFeKy?f$_u##)hDu0X_mwL6uKei~oPd9( zRaF_k&w(J3J8b_`F~?0(Ei_pH}U^c&r$uSYawB8Ybs-JZ|&;vKLWX! z|HFZ%-uBDaP*hMcQKf*|j5!b%H40SPD*#{A`kj|~esk@1?q}-O7WyAm3mD@-vHzw( zTSOlO(K9>GW;@?@xSwpk%X3Ui4_Psm;c*HF~RW+q+C#RO_VT5(x!5B#On-W`T|u z>>=t)W{=B-8wWZejxMaBC9sHzBZGv5uz_uu281kxHg2cll_sZBC&1AKD`CYh2vKeW zm#|MMdC}6A&^DX=>_(etx8f}9o}`(G?Y``M?D+aTPJbZqONmSs>y>WSbvs>7PE~cb zjO+1Y)PMi*!=06^$%< z*{b^66BIl{7zKvz^jut7ylDQBt)ba_F*$UkDgJ2gSNfHB6+`OEiz@xs$Tcrl>X4?o zu9~~b&Xl0?w(7lJXu8-9Yh6V|A3f?)1|~+u-q&6#YV`U2i?XIqUw*lc-QTXwuf@8d zSjMe1BhBKY`Mo{$s%Ce~Hv(^B{K%w{yndEtvyYjjbvFY^rn2>C1Lbi!3RV7F>&;zlSDSk}R>{twI}V zA~NK%T!z=^!qbw(OEgsmSj?#?GR&A$0&K>^(?^4iphc3rN_(xXA%joi)k~DmRLEXl zaWmwMolK%@YiyI|HvX{X$*Ei7y+zJ%m{b}$?N7_SN&p+FpeT%4Z_2`0CP=}Y3D-*@ zL|4W4ja#8*%SfkZzn5sfVknpJv&>glRk^oUqykedE8yCgIwCV)fC1iVwMr4hc#KcV!|M-r_N|nQWw@`j+0(Ywct~kLXQ)Qyncmi{Q4`Ur7A{Ep)n`zCtm8D zVX`kxa8Syc`g$6$($Qc-(_|LtQKWZXDrTir5s*pSVmGhk#dKJzCYT?vqA9}N9DGv> zw}N$byrt?Mk*ZZbN5&zb>pv;rU}EH@Rp54)vhZ=330bLvrKPEPu!WqR%yeM3LB!(E zw|J05Y!tajnZ9Ml*-aX&5T8YtuWDq@on)_*FMhz-?m|>RT0~e3OHllrEMthVY(KwQ zu>ijTc4>Xz-q1(g!ESjaZ+C+Zk5FgmF)rFX29_RmU!`7Pw+0}>8xK^=pOxtUDV)ok zw-=p=OvEH&VO3wToRdI!hPHc`qX+_{T_mj!NxcA&xOgkEuvz`-Aa`ZlNv>qnD0`YT1T3USO0ec!%{KE~UOGPJX%I5_rZDGx@|w zVIMsRPP+}^Xxa&{x!q{hY1wat8jDO7YP0(8xHWeEdrd79lUjB8%)v{X1pQu|1dr*y9M&a(J`038}4>lK&K zIM~6wnX{XA?pFHz{hOmEq{oYBnB@56twXqEcFrFqvCy)sH9B{pQ`G50o{W^t&onwY z-l{ur4#8ylPV5YRLD%%j^d0&_WI>0nmfZ8! zaZ&vo@7D`!=?215+Vk181*U@^{U>VyoXh2F&ZNzZx5tDDtlLc)gi2=|o=GC`uaH;< zFuuF?Q9Q`>S#c(~2p|s49RA`3242`2P+)F)t2N!CIrcl^0#gN@MLRDQ2W4S#MXZJO z8<(9P>MvW;rf2qZ$6sHxCVIr0B-gP?G{5jEDn%W#{T#2_&eIjvlVqm8J$*8A#n`5r zs6PuC!JuZJ@<8cFbbP{cRnIZs>B`?`rPWWL*A?1C3QqGEG?*&!*S0|DgB~`vo_xIo z&n_Sa(>6<$P7%Py{R<>n6Jy?3W|mYYoxe5h^b6C#+UoKJ(zl?^WcBn#|7wMI5=?S# zRgk8l-J`oM%GV&jFc)9&h#9mAyowg^v%Fc-7_^ou5$*YvELa!1q>4tHfX7&PCGqW* zu8In~5`Q5qQvMdToE$w+RP^_cIS2xJjghjCTp6Z(za_D<$S;0Xjt?mAE8~Ym{)zfb zV62v9|59XOvR}wEpm~Cnhyr`=JfC$*o15k?T`3s-ZqF6Gy;Gm+_6H$%oJPywWA^Wl zzn$L=N%{VT8DkQba0|2LqGR#O2Pw!b%LV4#Ojcx5`?Cm;+aLpkyZ=!r1z@E}V= z$2v6v%Ai)MMd`@IM&UD!%%(63VH8+m0Ebk<5Du#0=WeK(E<2~3@>8TceT$wy5F52n zRFtY>G9Gp~h#&R92{G{jLruZSNJ4)gNK+zg*$P zW@~Hf>_Do)tvfEAAMKE1nQ=8coTgog&S;wj(s?Xa0!r?UU5#2>18V#|tKvay1Ka53 zl$RxpMqrkv`Sv&#!_u8$8PMken`QL0_sD2)r&dZziefzSlAdKNKroVU;gRJE#o*}w zP_bO{F4g;|t!iroy^xf~(Q5qc8a3<+vBW%VIOQ1!??d;yEn1at1wpt}*n- z0iQtfu}Isw4ZfH~8p~#RQUKwf<$XeqUr-5?8TSqokdHL7tY|47R; z#d+4NS%Cqp>LQbvvAMIhcCX@|HozKXl)%*5o>P2ZegGuOerV&_MeA}|+o-3L!ZNJd z#1xB^(r!IfE~i>*5r{u;pIfCjhY^Oev$Y1MT16w8pJ0?9@&FH*`d;hS=c#F6fq z{mqsHd*xa;>Hg?j80MwZ%}anqc@&s&2v{vHQS68fueNi5Z(VD2eH>jmv4uvE|HEQm z^=b&?1R9?<@=kjtUfm*I!wPf5Xnma(4*DfPk}Es*H$%NGCIM1qt(LSvbl7&tV>e2$ zUqvZOTiwQyxDoxL(mn?n_x%Tre?L&!FYCOy0>o}#DTC3uSPnyGBv*}!*Yv5IV)Bg_t%V+UrTXfr!Q8+eX}ANR*YLzwme7Rl z@q_*fP7wP2AZ(3WG*)4Z(q@)~c{Je&7?w^?&Wy3)v0{TvNQRGle9mIG>$M2TtQ(Vf z3*PV@1mX)}beRTPjoG#&&IO#Mn(DLGp}mn)_0e=9kXDewC8Pk@yo<8@XZjFP-_zic z{mocvT9Eo)H4Oj$>1->^#DbbiJn^M4?v7XbK>co+v=7g$hE{#HoG6ZEat!s~I<^_s zlFee93KDSbJKlv_+GPfC6P8b>(;dlJ5r9&Pc4kC2uR(0{Kjf+SMeUktef``iXD}8` zGufkM9*Sx4>+5WcK#Vqm$g#5z1DUhc_#gLGe4_icSzN5GKr|J&eB)LS;jTXWA$?(k zy?*%U9Q#Y88(blIlxrtKp6^jksNF>-K1?8=pmYAPj?qq}yO5L>_s8CAv=LQMe3J6? zOfWD>Kx_5A4jRoIU}&aICTgdYMqC|45}St;@0~7>Af+uK3vps9D!9qD)1;Y6Fz>4^ zR1X$s{QNZl7l%}Zwo2wXP+Cj-K|^wqZW?)s1WUw_APZLhH55g{wNW3liInD)WHh${ zOz&K>sB*4inVY3m)3z8w!yUz+CKF%_-s2KVr7DpwTUuZjPS9k-em^;>H4*?*B0Bg7 zLy2nfU=ac5N}x1+Tlq^lkNmB~Dj+t&l#fO&%|7~2iw*N!*xBy+ZBQ>#g_;I*+J{W* z=@*15><)Bh9f>>dgQrEhkrr2FEJ;R2rH%`kda8sD-FY6e#7S-<)V*zQA>)Ps)L- zgUuu@5;Ych#jX_KZ+;qEJJbu{_Z9WSsLSo#XqLpCK$gFidk}gddW(9$v}iyGm_OoH ztn$pv81zROq686_7@avq2heXZnkRi4n(3{5jTDO?9iP%u8S4KEqGL?^uBeg(-ws#1 z9!!Y_2Q~D?gCL3MQZO!n$+Wy(Twr5AS3{F7ak2f)Bu0iG^k^x??0}b6l!>Vjp{e*F z8r*(Y?3ZDDoS1G?lz#J4`d9jAEc9YGq1LbpYoFl!W!(j8-33Ey)@yx+BVpDIVyvpZ zq5QgKy>P}LlV?Bgy@I)JvefCG)I69H1;q@{8E8Ytw^s-rC7m5>Q>ZO(`$`9@`49s2)q#{2eN0A?~qS8%wxh%P*99h*Sv` zW_z3<=iRZBQKaDsKw^TfN;6`mRck|6Yt&e$R~tMA0ix;qgw$n~fe=62aG2v0S`7mU zI}gR#W)f+Gn=e3mm*F^r^tcv&S`Rym`X`6K`i8g-a0!p|#69@Bl!*&)QJ9(E7ycxz z)5-m9v`~$N1zszFi^=m%vw}Y{ZyYub!-6^KIY@mwF|W+|t~bZ%@rifEZ-28I@s$C` z>E+k~R1JC-M>8iC_GR>V9f9+uL2wPRATL9bC(sxd;AMJ>v6c#PcG|Xx1N5^1>ISd0 z4%vf-SNOw+1%yQq1YP`>iqq>5Q590_pr?OxS|HbLjx=9~Y)QO37RihG%JrJ^=Nj>g zPTcO$6r{jdE_096b&L;Wm8vcxUVxF0mA%W`aZz4n6XtvOi($ zaL!{WUCh&{5ar=>u)!mit|&EkGY$|YG<_)ZD)I32uEIWwu`R-_ z`FVeKyrx3>8Ep#2~%VVrQ%u#exo!anPe`bc)-M=^IP1n1?L2UQ@# zpNjoq-0+XCfqXS!LwMgFvG$PkX}5^6yxW)6%`S8{r~BA2-c%-u5SE#%mQ~5JQ=o$c z%+qa0udVq9`|=2n=0k#M=yiEh_vp?(tB|{J{EhVLPM^S@f-O*Lgb390BvwK7{wfdMKqUc0uIXKj5>g^z z#2`5^)>T73Eci+=E4n&jl42E@VYF2*UDiWLUOgF#p9`E4&-A#MJLUa&^hB@g7KL+n zr_bz+kfCcLIlAevILckIq~RCwh6dc5@%yN@#f3lhHIx4fZ_yT~o0#3@h#!HCN(rHHC6#0$+1AMq?bY~(3nn{o5g8{*e_#4RhW)xPmK zTYBEntuYd)`?`bzDksI9*MG$=^w!iiIcWg1lD&kM1NF@qKha0fDVz^W7JCam^!AQFxY@7*`a3tfBwN0uK_~YBQ18@^i%=YB}K0Iq(Q3 z=7hNZ#!N@YErE7{T|{kjVFZ+f9Hn($zih;f&q^wO)PJSF`K)|LdT>!^JLf=zXG>>G z15TmM=X`1%Ynk&dvu$Vic!XyFC(c=qM33v&SIl|p+z6Ah9(XQ0CWE^N-LgE#WF6Z+ zb_v`7^Rz8%KKg_@B>5*s-q*TVwu~MCRiXvVx&_3#r1h&L+{rM&-H6 zrcgH@I>0eY8WBX#Qj}Vml+fpv?;EQXBbD0lx%L?E4)b-nvrmMQS^}p_CI3M24IK(f| zV?tWzkaJXH87MBz^HyVKT&oHB;A4DRhZy;fIC-TlvECK)nu4-3s7qJfF-ZZGt7+6C3xZt!ZX4`M{eN|q!y*d^B+cF5W- zc9C|FzL;$bAfh56fg&y0j!PF8mjBV!qA=z$=~r-orU-{0AcQUt4 zNYC=_9(MOWe$Br9_50i#0z!*a1>U6ZvH>JYS9U$kkrCt7!mEUJR$W#Jt5vT?U&LCD zd@)kn%y|rkV|CijnZ((B2=j_rB;`b}F9+E1T46sg_aOPp+&*W~44r9t3AI}z)yUFJ z+}z5E6|oq+oPC3Jli)EPh9)o^B4KUYkk~AU9!g`OvC`a!#Q>JmDiMLTx>96_iDD9h@nW%Je4%>URwYM%5YU1&Dcdulvv3IH3GSrA4$)QjlGwUt6 zsR6+PnyJ$1x{|R=ogzErr~U|X!+b+F8=6y?Yi`E$yjWXsdmxZa^hIqa)YV9ubUqOj&IGY}bk zH4*DEn({py@MG5LQCI;J#6+98GaZYGW-K-&C`(r5#?R0Z){DlY8ZZk}lIi$xG}Q@2 z0LJhzuus-7dLAEpG1Lf+KOxn&NSwO{wn_~e0=}dovX)T(|WRMTqacoW8;A>8tTDr+0yRa+U!LW z!H#Gnf^iCy$tTk3kBBC=r@xhskjf1}NOkEEM4*r+A4`yNAIjz`_JMUI#xTf$+{UA7 zpBO_aJkKz)iaKqRA{8a6AtpdUwtc#Y-hxtZnWz~i(sfjMk`lq|kGea=`62V6y)TMPZw8q}tFDDHrW_n(Z84ZxWvRrntcw;F|Mv4ff9iaM% z4IM{=*zw}vIpbg=9%w&v`sA+a3UV@Rpn<6`c&5h+8a7izP>E@7CSsCv*AAvd-izwU z!sGJQ?fpCbt+LK`6m2Z3&cKtgcElAl){*m0b^0U#n<7?`8ktdIe#ytZTvaZy728o6 z3GDmw=vhh*U#hCo0gb9s#V5(IILXkw>(6a?BFdIb0%3~Y*5FiMh&JWHd2n(|y@?F8 zL$%!)uFu&n+1(6)oW6Hx*?{d~y zBeR)N*Z{7*gMlhMOad#k4gf`37OzEJ&pH?h!Z4#mNNCfnDI@LbiU~&2Gd^q7ix8~Y6$a=B9bK(BaTEO0$Oh=VCkBPwt0 zf#QuB25&2!m7MWY5xV_~sf(0|Y*#Wf8+FQI(sl2wgdM5H7V{aH6|ntE+OcLsTC`u; zeyrlkJgzdIb5=n#SCH)+kjN)rYW7=rppN3Eb;q_^8Zi}6jtL@eZ2XO^w{mCwX(q!t ztM^`%`ndZ5c+2@?p>R*dDNeVk#v>rsn>vEo;cP2Ecp=@E>A#n0!jZACKZ1=D0`f|{ zZnF;Ocp;$j86m}Gt~N+Ch6CJo7+Wzv|nlsXBvm z?St-5Ke&6hbGAWoO!Z2Rd8ARJhOY|a1rm*sOif%Th`*=^jlgWo%e9`3sS51n*>+Mh(9C7g@*mE|r%h*3k6I_uo;C!N z7CVMIX4kbA#gPZf_0%m18+BVeS4?D;U$QC`TT;X zP#H}tMsa=zS6N7n#BA$Fy8#R7vOesiCLM@d1UO6Tsnwv^gb}Q9I}ZQLI?--C8ok&S z9Idy06+V(_aj?M78-*vYBu|AaJ9mlEJpFEIP}{tRwm?G{ag>6u(ReBKAAx zDR6qe!3G88NQP$i99DZ~CW9lzz}iGynvGA4!yL}_9t`l*SZbEL-%N{n$%JgpDHJRn zvh<{AqR7z@ylV`kXdk+uEu-WWAt^=A4n(J=A1e8DpeLzAd;Nl#qlmp#KcHU!8`YJY zvBZy@>WiBZpx*wQ8JzKw?@k}8l99Wo&H>__vCFL}>m~MTmGvae% zPTn9?iR=@7NJ)?e+n-4kx$V#qS4tLpVUX*Je0@`f5LICdxLnph&Vjbxd*|+PbzS(l zBqqMlUeNoo8wL&_HKnM^8{iDI3IdzJAt32UupSr6XXh9KH2LjWD)Pz+`cmps%eHeD zU%i1SbPuSddp6?th;;DfUlxYnjRpd~i7vQ4V`cD%4+a9*!{+#QRBr5^Q$5Ec?gpju zv@dk9;G>d7QNEdRy}fgeA?i=~KFeibDtYffy)^OP?Ro~-X!onDpm+uGpe&6)*f@xJ zE1I3Qh}`1<7aFB@TS#}ee={<#9%1wOL%cuvOd($y4MC2?`1Nin=pVLXPkknn*0kx> z!9XHW${hYEV;r6F#iz7W=fg|a@GY0UG5>>9>$3Bj5@!N{nWDD`;JOdz_ZaZVVIUgH zo+<=+n8VGL*U%M|J$A~#ll__<`y+jL>bv;TpC!&|d=q%E2B|5p=)b-Q+ZrFO%+D_u z4%rc8BmOAO6{n(i(802yZW93?U;K^ZZlo0Gvs7B+<%}R;$%O}pe*Gi;!xP-M73W`k zXLv473Ex_VPcM-M^JO|H>KD;!sEGJ|E}Qepen;yNG2 zXqgD5sjQUDI(XLM+^8ZX1s_(X+PeyQ$Q5RukRt|Kwr-FSnW!^9?OG64UYX1^bU9d8 zJ}8K&UEYG+Je^cThf8W*^RqG07nSCmp*o5Z;#F zS?jochDWX@p+%CZ%dOKUl}q{9)^U@}qkQtA3zBF)`I&zyIKgb{mv)KtZ}?_h{r#VZ z%C+hwv&nB?we0^H+H`OKGw-&8FaF;=ei!tAclS5Q?qH9J$nt+YxdKkbRFLnWvn7GH zezC6<{mK0dd763JlLFqy&Oe|7UXII;K&2pye~yG4jldY~N;M9&rX}m76NsP=R#FEw zt(9h+=m9^zfl=6pH*D;JP~OVgbJkXh(+2MO_^;%F{V@pc2nGn~=U)Qx|JEV-e=vXk zPxA2J<9~IH{}29#X~KW$(1reJv}lc4_1JF31gdev>!CddVhf_62nsr6%w)?IWxz}{ z(}~~@w>c07!r=FZANq4R!F2Qi2?QGavZ{)PCq~X}3x;4ylsd&m;dQe;0GFSn5 zZ*J<=Xg1fEGYYDZ0{Z4}Jh*xlXa}@412nlKSM#@wjMM z*0(k>Gfd1Mj)smUuX}EM6m)811%n5zzr}T?$ZzH~*3b`3q3gHSpA<3cbzTeRDi`SA zT{O)l3%bH(CN0EEF9ph1(Osw5y$SJolG&Db~uL!I3U{X`h(h%^KsL71`2B1Yn z7(xI+Fk?|xS_Y5)x?oqk$xmjG@_+JdErI(q95~UBTvOXTQaJs?lgrC6Wa@d0%O0cC zzvslIeWMo0|C0({iEWX{=5F)t4Z*`rh@-t0ZTMse3VaJ`5`1zeUK0~F^KRY zj2z-gr%sR<(u0@SNEp%Lj38AB2v-+cd<8pKdtRU&8t3eYH#h7qH%bvKup4cnnrN>l z!5fve)~Y5_U9US`uXDFoOtx2gI&Z!t&VPIoqiv>&H(&1;J9b}kZhcOX7EiW*Bujy#MaCl52%NO-l|@2$aRKvZ!YjwpXwC#nA(tJtd1p?jx&U|?&jcb!0MT6oBlWurVRyiSCX?sN3j}d zh3==XK$^*8#zr+U^wk(UkF}bta4bKVgr`elH^az{w(m}3%23;y7dsEnH*pp{HW$Uk zV9J^I9ea7vp_A}0F8qF{>|rj`CeHZ?lf%HImvEJF<@7cgc1Tw%vAUA47{Qe(sP^5M zT=z<~l%*ZjJvObcWtlN?0$b%NdAj&l`Cr|x((dFs-njsj9%IIqoN|Q?tYtJYlRNIu zY(LtC-F14)Og*_V@gjGH^tLV4uN?f^#=dscCFV~a`r8_o?$gj3HrSk=YK2k^UW)sJ z&=a&&JkMkWshp0sto$c6j8f$J!Bsn*MTjC`3cv@l@7cINa!}fNcu(0XF7ZCAYbX|WJIL$iGx8l zGFFQsw}x|i!jOZIaP{@sw0BrV5Z5u!TGe@JGTzvH$}55Gf<;rieZlz+6E1}z_o3m2 z(t;Cp^Geen7iSt)ZVtC`+tzuv^<6--M`^5JXBeeLXV)>2;f7=l%(-4?+<5~;@=Th{1#>rK3+rLn(44TAFS@u(}dunUSYu}~))W*fr` zkBL}3k_@a4pXJ#u*_N|e#1gTqxE&WPsfDa=`@LL?PRR()9^HxG?~^SNmeO#^-5tMw zeGEW&CuX(Uz#-wZOEt8MmF}hQc%14L)0=ebo`e$$G6nVrb)afh!>+Nfa5P;N zCCOQ^NRel#saUVt$Ds0rGd%gkKP2LsQRxq6)g*`-r(FGM!Q51c|9lk!ha8Um3ys1{ zWpT7XDWYshQ{_F!8D8@3hvXhQDw;GlkUOzni&T1>^uD){WH3wRONgjh$u4u7?+$(Y zqTXEF>1aPNZCXP0nJ;zs6_%6;+D&J_|ugcih**y(4ApT`RKAi5>SZe0Bz|+l7z>P14>0ljIH*LhK z@}2O#{?1RNa&!~sEPBvIkm-uIt^Pt#%JnsbJ`-T0%pb ze}d;dzJFu7oQ=i`VHNt%Sv@?7$*oO`Rt*bRNhXh{FArB`9#f%ksG%q?Z`_<19;dBW z5pIoIo-JIK9N$IE1)g8@+4}_`sE7;Lus&WNAJ^H&=4rGjeAJP%Dw!tn*koQ&PrNZw zY88=H7qpHz11f}oTD!0lWO>pMI;i4sauS`%_!zM!n@91sLH#rz1~iEAu#1b%LA zhB}7{1(8{1{V8+SEs=*f=FcRE^;`6Pxm$Hie~|aD~W1BYy#@Y$C?pxJh*cC!T@8C9{xx*T*8P zhbkRk3*6)Zbk%}u>^?ItOhxdmX$j9KyoxxN>NrYGKMkLF4*fLsL_PRjHNNHCyaUHN z7W8yEhf&ag07fc9FD>B{t0#Civsoy0hvVepDREX(NK1LbK0n*>UJp&1FygZMg7T^G z(02BS)g#qMOI{RJIh7}pGNS8WhSH@kG+4n=(8j<+gVfTur)s*hYus70AHUBS2bN6Zp_GOHYxsbg{-Rcet{@0gzE`t$M0_!ZIqSAIW53j+Ln7N~8J zLZ0DOUjp^j`MvX#hq5dFixo^1szoQ=FTqa|@m>9F@%>7OuF9&_C_MDco&-{wfLKNrDMEN4pRUS8-SD6@GP`>_7$;r>dJo>KbeXm>GfQS? zjFS+Y6^%pDCaI0?9(z^ELsAE1`WhbhNv5DJ$Y}~r;>FynHjmjmA{bfDbseZXsKUv`%Fekv)1@f%7ti;B5hhs}5db1dP+P0${1DgKtb(DvN}6H6;0*LP6blg*rpr;Z(7? zrve>M`x6ZI(wtQc4%lO?v5vr{0iTPl&JT!@k-7qUN8b$O9YuItu7zrQ*$?xJIN#~b z#@z|*5z&D7g5>!o(^v+3N?JnJns5O2W4EkF>re*q1uVjgT#6ROP5>Ho)XTJoHDNRC zuLC(Cd_ZM?FAFPoMw;3FM4Ln0=!+vgTYBx2TdXpM@EhDCorzTS6@2`swp4J^9C0)U zq?)H8)=D;i+H`EVYge>kPy8d*AxKl};iumYu^UeM+e_3>O+LY`D4?pD%;Vextj!(; zomJ(u+dR(0m>+-61HTV7!>03vqozyo@uY@Zh^KrW`w7^ENCYh86_P2VC|4}(ilMBe zwa&B|1a7%Qkd>d14}2*_yYr@8-N}^&?LfSwr)C~UUHr)ydENu=?ZHkvoLS~xTiBH= zD%A=OdoC+10l7@rXif~Z#^AvW+4M-(KQBj=Nhgts)>xmA--IJf1jSZF6>@Ns&nmv} zXRk`|`@P5_9W4O-SI|f^DCZ-n*yX@2gf6N)epc~lRWl7QgCyXdx|zr^gy>q`Vwn^y z&r3_zS}N=HmrVtTZhAQS`3$kBmVZDqr4+o(oNok?tqel9kn3;uUerFRti=k+&W{bb zT{ZtEf51Qf+|Jc*@(nyn#U+nr1SFpu4(I7<1a=)M_yPUAcKVF+(vK!|DTL2;P)yG~ zrI*7V)wN_92cM)j`PtAOFz_dO)jIfTeawh2{d@x0nd^#?pDkBTBzr0Oxgmvjt`U^$ zcTPl=iwuen=;7ExMVh7LLFSKUrTiPJpMB&*Ml32>wl} zYn(H0N4+>MCrm2BC4p{meYPafDEXd4yf$i%ylWpC|9%R4XZBUQiha(x%wgQ5iJ?K_wQBRfw z+pYuKoIameAWV7Ex4$PCd>bYD7)A9J`ri&bwTRN*w~7DR0EeLXW|I2()Zkl6vxiw? zFBX){0zT@w_4YUT4~@TXa;nPb^Tu$DJ=vluc~9)mZ}uHd#4*V_eS7)^eZ9oI%Wws_ z`;97^W|?_Z6xHSsE!3EKHPN<3IZ^jTJW=Il{rMmlnR#OuoE6dqOO1KOMpW84ZtDHNn)(pYvs=frO`$X}sY zKY0At$G85&2>B|-{*+B*aqQn&Mqjt*DVH2kdwEm5f}~Xwn9+tPt?EPwh8=8=VWA8rjt*bHEs1FJ92QohQ)Y z4sQH~AzB5!Pisyf?pVa0?L4gthx2;SKlrr?XRU`?Y>RJgUeJn!az#sNF7oDbzksrD zw8)f=f1t*UK&$}_ktf!yf4Rjt{56ffTA{A=9n})E7~iXaQkE+%GW4zqbmlYF(|hE@ z421q9`UQf$uA5yDLx67`=EnSTxdEaG!6C%9_obpb?;u-^QFX% zU1wQ}Li{PeT^fS;&Sk2#$ZM#Zpxrn7jsd<@qhfWy*H)cw9q!I9!fDOCw~4zg zbW`EHsTp9IQUCETUse)!ZmuRICx}0Oe1KVoqdK+u>67A8v`*X*!*_i5`_qTzYRkbYXg#4vT5~A{lK#bA}Oc4ePu5hr-@;i%Z!4Y;-(yR z(1rHYTc7i1h1aipP4DaIY3g2kF#MX{XW7g&zL!39ohO98=eo5nZtq+nz}2E$OZpxx z&OFaOM1O;?mxq+`%k>YS!-=H7BB&WhqSTUC{S!x*k9E zcB;u0I!h%3nEchQwu1GnNkaQxuWnW0D@Xq5j@5WE@E(WlgDU;FLsT*eV|Bh)aH0;~@^yygFj<=+Vu3p)LlF%1AA%y5z-Oh`2 z$RDKk_6r+f#I`8fQ%y#Wx%~de1qkWL2(q^~veLKwht-dIcpt(@lc>`~@mISRIPKPm zD!Za&aX@7dy*CT!&Z7JC1jP2@8+ro8SmlH>_gzRte%ojgiwfd?TR+%Ny0`sp`QRLy zl5TiQkFhIC!2aaJ&=Ua`c9UuOk9GkSFZ}!IGeMZ5MXrL zGtMj`m{(X9+l%=d|L zW2OY?8!_pyhvJ1@O!Chsf6}@3HmKq@)x;CFItPMpkSr@npO&8zMc_O?*|sqkuL^U? zV9+x3vbr|6;Ft0J^J>IH_xpa<{S5K?u-sQWC7FB9YFMwoCKK3WZ*gvO-wAApF`K%#7@1 z^sEj4*%hH`f0@sRDGI|#Dl20o$Z*gttP$q(_?#~2!H9(!d=)I93-3)?e%@$1^*F=t9t&OQ9!p84Z`+y<$yQ9wlamK~Hz2CRpS8dWJfBl@(M2qX!9d_F= zd|4A&U~8dX^M25wyC7$Swa22$G61V;fl{%Q4Lh!t_#=SP(sr_pvQ=wqOi`R)do~QX zk*_gsy75$xoi5XE&h7;-xVECk;DLoO0lJ3|6(Ba~ezi73_SYdCZPItS5MKaGE_1My zdQpx?h&RuoQ7I=UY{2Qf ziGQ-FpR%piffR_4X{74~>Q!=i`)J@T415!{8e`AXy`J#ZK)5WWm3oH?x1PVvcAqE@ zWI|DEUgxyN({@Y99vCJVwiGyx@9)y2jNg`R{$s2o;`4!^6nDX_pb~fTuzf>ZoPV@X zXKe1ehcZ+3dxCB+vikgKz8pvH?>ZzlOEObd{(-aWY;F0XIbuIjSA+!%TNy87a>BoX zsae$}Fcw&+)z@n{Fvzo;SkAw0U*}?unSO)^-+sbpNRjD8&qyfp%GNH;YKdHlz^)4( z;n%`#2Pw&DPA8tc)R9FW7EBR3?GDWhf@0(u3G4ijQV;{qp3B)`Fd}kMV}gB2U%4Sy z3x>YU&`V^PU$xWc4J!OG{Jglti@E3rdYo62K31iu!BU&pdo}S66Ctq{NB<88P92Y9 zTOqX$h6HH_8fKH(I>MEJZl1_2GB~xI+!|BLvN;CnQrjHuh?grzUO7h;1AbzLi|_O= z2S=(0tX#nBjN92gRsv;7`rDCATA!o(ZA}6)+;g;T#+1~HXGFD1@3D#|Ky9!E@)u=h z3@zg3Us0BCYmq(pB`^QTp|RB9!lX*{;7r|Z(^>J+av(0-oUmIdR78c4(q%hP#=R@W ze{;yy$T^8kXr(oC*#NQMZSQlgU)aa=BrZDwpLUk5tm&(AkNt&Gel`=ydcL*<@Ypx{ z2uOxl>2vSY2g3%Si&JU<9D5#{_z{9PzJh=miNH;STk^;5#%8iMRfPe#G~T>^U_zt? zgSE)`UQhb!G$at%yCf5MU)<&(L73(hY3*%qqPbX;`%QDHed3ZaWw^k)8Vjd#ePg@;I&pMe+A18k+S+bou|QX?8eQ`{P-0vrm=uR;Y(bHV>d>Gen4LHILqcm_ z3peDMRE3JMA8wWgPkSthI^K<|8aal38qvIcEgLjHAFB0P#IfqP2y}L>=8eBR}Fm^V*mw2Q4+o=exP@*#=Zs zIqHh@neG)Vy%v4cB1!L}w9J>IqAo}CsqbFPrUVc@;~Ld7t_2IIG=15mT7Itrjq#2~ zqX*&nwZP>vso$6W!#` z-YZ}jhBwQku-Qc>TIMpn%_z~`^u4v3Skyf)KA}V{`dr!Q;3xK1TuGYdl}$sKF^9X!*a-R*Oq1#tLq!W)gO}{q`1HM;oh1-k4FU@8W(qe>P05$+ z`ud2&;4IW4vq8#2yA{G>OH=G+pS_jctJ*BqD$j-MI#avR+<>m-`H1@{3VgKYn2_Ih z0`2_1qUMRuzgj_V^*;5Ax_0s{_3tYR>|$i#c!F7)#`oVGmsD*M2?%930cBSI4Mj>P zTm&JmUrvDXlB%zeA_7$&ogjGK3>SOlV$ct{4)P0k)Kua%*fx9?)_fkvz<(G=F`KCp zE`0j*=FzH$^Y@iUI}MM2Hf#Yr@oQdlJMB5xe0$aGNk%tgex;0)NEuVYtLEvOt{}ti zL`o$K9HnnUnl*;DTGTNiwr&ydfDp@3Y)g5$pcY9l1-9g;yn6SBr_S9MV8Xl+RWgwb zXL%kZLE4#4rUO(Pj484!=`jy74tQxD0Zg>99vvQ}R$7~GW)-0DVJR@$5}drsp3IQG zlrJL}M{+SdWbrO@+g2BY^a}0VdQtuoml`jJ2s6GsG5D@(^$5pMi3$27psEIOe^n=*Nj|Ug7VXN0OrwMrRq&@sR&vdnsRlI%*$vfmJ~)s z^?lstAT$Ked`b&UZ@A6I<(uCHGZ9pLqNhD_g-kj*Sa#0%(=8j}4zd;@!o;#vJ+Bsd z4&K4RIP>6It9Ir)ey?M6Gi6@JzKNg;=jM=$)gs2#u_WhvuTRwm1x2^*!e%l&j02xz zYInQgI$_V7Epzf3*BU~gos}|EurFj8l}hsI(!5yX!~ECL%cnYMS-e<`AKDL%(G)62 zPU;uF1(~(YbH2444JGh58coXT>(*CdEwaFuyvB|%CULgVQesH$ znB`vk3BMP<-QauWOZ0W6xB5y7?tE5cisG|V;bhY^8+*BH1T0ZLbn&gi12|a9Oa%;I zxvaxX_xe3@ng%;4C?zPHQ1v%dbhjA6Sl7w<*)Nr#F{Ahzj}%n9c&!g5HVrlvUO&R2C)_$x6M9 zahficAbeHL2%jILO>Pq&RPPxl;i{K5#O*Yt15AORTCvkjNfJ)LrN4K{sY7>tGuTQ@ z^?N*+xssG&sfp0c$^vV*H)U1O!fTHk8;Q7@42MT@z6UTd^&DKSxVcC-1OLjl7m63& zBb&goU!hes(GF^yc!107bkV6Pr%;A-WWd@DK2;&=zyiK*0i^0@f?fh2c)4&DRSjrI zk!W^=l^JKlPW9US{*yo?_XT@T2Bx+Cm^+r{*5LVcKVw*ll3+)lkebA-4)o z8f5xHWOx0!FDSs4nv@o@>mxTQrOeKzj@5uL`d>mXSp|#{FE54EE_!KtQNq>-G(&5) ztz?xkqPU16A-8@-quJ|SU^ClZ?bJ2kCJPB|6L>NTDYBprw$WcwCH{B z5qlJ6wK_9sT@Kl6G|Q&$gsl@WT>hE;nDAbH#%f1ZwuOkvWLj{qV$m3LF423&l!^iV zhym*>R>Yyens++~6F5+uZQTCz9t~PEW+e?w)XF2g!^^%6k?@Jcu;MG0FG9!T+Gx{Z zK;31y@(J{!-$k4E{5#Sv(2DGy3EZQY}G_*z*G&CZ_J?m&Fg4IBrvPx1w z1zAb3k}6nT?E)HNCi%}aR^?)%w-DcpBR*tD(r_c{QU6V&2vU-j0;{TVDN6los%YJZ z5C(*ZE#kv-BvlGLDf9>EO#RH_jtolA)iRJ>tSfJpF!#DO+tk% zBAKCwVZwO^p)(Rhk2en$XLfWjQQ`ix>K}Ru6-sn8Ih6k&$$y`zQ}}4dj~o@9gX9_= z#~EkchJqd5$**l}~~6mOl(q#GMIcFg&XCKO;$w>!K14 zko1egAORiG{r|8qj*FsN>?7d`han?*MD#xe^)sOqj;o;hgdaVnBH$BM{_73?znS+R z*G2VHM!Jw6#<FfJ-J%-9AuDW$@mc-Eyk~F{Jbvt` zn;(%DbBDnKIYr~|I>ZTvbH@cxUyw%bp*)OSs}lwO^HTJ2M#u5QsPF0?Jv*OVPfdKv z+t$Z5P!~jzZ~Y!d#iP?S{?M_g%Ua0Q)WawbIx+2uYpcf(7Im%W=rAu4dSceo7RZh# zN38=RmwOJQE$qbPXIuO^E`wSeJKCx3Q76irp~QS#19dusEVCWPrKhK9{7cbIMg9U} TZiJi*F`$tkWLn) literal 0 HcmV?d00001 diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties index a4413138..09523c0e 100644 --- a/backend/gradle/wrapper/gradle-wrapper.properties +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/backend/gradlew b/backend/gradlew old mode 100644 new mode 100755 index b740cf13..f5feea6d --- a/backend/gradlew +++ b/backend/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/backend/gradlew.bat b/backend/gradlew.bat index e69de29b..9b42019c 100644 --- a/backend/gradlew.bat +++ b/backend/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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 From 5e38631c6d18679db361f2dd040f5afe07e22fb7 Mon Sep 17 00:00:00 2001 From: libienz Date: Mon, 15 Jul 2024 20:06:54 +0900 Subject: [PATCH 006/108] =?UTF-8?q?fix:=20=EB=8F=84=EC=BB=A4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/be-dev-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/be-dev-ci.yml b/.github/workflows/be-dev-ci.yml index e64bb801..df4f8722 100644 --- a/.github/workflows/be-dev-ci.yml +++ b/.github/workflows/be-dev-ci.yml @@ -21,6 +21,9 @@ jobs: distribution: 'temurin' java-version: '17' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -28,4 +31,4 @@ jobs: run: ./gradlew clean build - name: Build the Docker image - run: docker build -f ./backend/Dockerfile --no-cache -t touroot/touroot-api . + run: docker build -f Dockerfile --no-cache -t touroot/touroot-api . From c34b0f153e7d505f23f2782b46ce33040a0957f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Mon, 15 Jul 2024 20:45:27 +0900 Subject: [PATCH 007/108] =?UTF-8?q?[Feature]=20-=20Docker=20Push=20/=20Doc?= =?UTF-8?q?ker=20Build=20CI=20workflow=20=EC=B6=94=EA=B0=80=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 도커 이미지 build & push workflow 작성 * fix: 도커 이미지 허브에 추가하는 과정의 오류 해결 --- .github/workflows/be-dev-ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/be-dev-ci.yml b/.github/workflows/be-dev-ci.yml index df4f8722..0193efc0 100644 --- a/.github/workflows/be-dev-ci.yml +++ b/.github/workflows/be-dev-ci.yml @@ -30,5 +30,14 @@ jobs: - name: Build with Gradle run: ./gradlew clean build + - name: Sign in Dockerhub + uses: docker/login-action@v1 + with: + username: ${{secrets.DOCKER_USERNAME}} + password: ${{secrets.DOCKER_PASSWORD}} + - name: Build the Docker image - run: docker build -f Dockerfile --no-cache -t touroot/touroot-api . + run: docker build -f ./Dockerfile --no-cache -t touroot/touroot-api . + + - name: Push the Docker Image to Dockerhub + run: docker push touroot/touroot-api From 1019e887598844100fbc8adb2e1c58ec24551e56 Mon Sep 17 00:00:00 2001 From: eunjungL Date: Mon, 15 Jul 2024 20:56:33 +0900 Subject: [PATCH 008/108] =?UTF-8?q?fix:=20be-dev-ci=20docker=20image=20?= =?UTF-8?q?=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/be-dev-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/be-dev-ci.yml b/.github/workflows/be-dev-ci.yml index 0193efc0..12ba5209 100644 --- a/.github/workflows/be-dev-ci.yml +++ b/.github/workflows/be-dev-ci.yml @@ -37,7 +37,7 @@ jobs: password: ${{secrets.DOCKER_PASSWORD}} - name: Build the Docker image - run: docker build -f ./Dockerfile --no-cache -t touroot/touroot-api . + run: docker build -f ./Dockerfile --platform linux/arm64 --no-cache -t touroot/touroot-api . - name: Push the Docker Image to Dockerhub run: docker push touroot/touroot-api From ac087fe1311c336b88837a8ea95c7d312e57bc76 Mon Sep 17 00:00:00 2001 From: eunjungL Date: Mon, 15 Jul 2024 21:09:48 +0900 Subject: [PATCH 009/108] =?UTF-8?q?feat:=20Ping=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/woowacourse/touroot/PingController.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/PingController.java diff --git a/backend/src/main/java/woowacourse/touroot/PingController.java b/backend/src/main/java/woowacourse/touroot/PingController.java new file mode 100644 index 00000000..9d9e0585 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/PingController.java @@ -0,0 +1,15 @@ +package woowacourse.touroot; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/ping") +public class PingController { + + @GetMapping + public String ping() { + return "pong"; + } +} From a961ff322d1844bb8c535fd2290fe68cec9e9113 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Mon, 15 Jul 2024 21:56:36 +0900 Subject: [PATCH 010/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20Entity=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?#17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../touroot/place/domain/Place.java | 28 +++++++++++++ .../touroot/travelogue/domain/Travelogue.java | 29 +++++++++++++ .../travelogueday/domain/TravelogueDay.java | 34 +++++++++++++++ .../domain/TraveloguePhoto.java | 31 ++++++++++++++ .../domain/TraveloguePlace.java | 41 +++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/place/domain/Place.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java create mode 100644 backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java diff --git a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java new file mode 100644 index 00000000..25cb57f9 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java @@ -0,0 +1,28 @@ +package woowacourse.touroot.place.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; + +@Getter +@Entity +public class Place { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private Double latitude; + + @Column(nullable = false) + private Double longitude; + + private String googlePlaceId; +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java new file mode 100644 index 00000000..1899e848 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java @@ -0,0 +1,29 @@ +package woowacourse.touroot.travelogue.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import java.util.List; +import lombok.Getter; +import woowacourse.touroot.travelogueday.domain.TravelogueDay; + +@Getter +@Entity +public class Travelogue { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 20) + private String title; + + @Column(nullable = false) + private String thumbnail; + + @OneToMany(mappedBy = "travelogue") + private List days; +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java b/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java new file mode 100644 index 00000000..2123b5b9 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java @@ -0,0 +1,34 @@ +package woowacourse.touroot.travelogueday.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.List; +import lombok.Getter; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; + +@Getter +@Entity +public class TravelogueDay { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Integer order; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Travelogue travelogue; + + @OneToMany(mappedBy = "travelogueDay") + private List places; +} diff --git a/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java b/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java new file mode 100644 index 00000000..9bb79e4e --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java @@ -0,0 +1,31 @@ +package woowacourse.touroot.traveloguephoto.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; + +@Getter +@Entity +public class TraveloguePhoto { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String key; + + @Column(nullable = false) + private Integer order; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private TraveloguePlace place; +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java b/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java new file mode 100644 index 00000000..4b932fa2 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java @@ -0,0 +1,41 @@ +package woowacourse.touroot.travelogueplace.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.List; +import lombok.Getter; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelogueday.domain.TravelogueDay; +import woowacourse.touroot.traveloguephoto.domain.TraveloguePhoto; + +@Getter +@Entity +public class TraveloguePlace { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Integer order; + + private String description; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Place place; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private TravelogueDay day; + + @OneToMany(mappedBy = "traveloguePlace") + private List photos; +} From d0ad948bfdc260f5ed023a7d3ad113ceb02686a2 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Mon, 15 Jul 2024 22:23:15 +0900 Subject: [PATCH 011/108] =?UTF-8?q?[Fix]=20-=20Database=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=EC=96=B4=EC=98=80=EB=8D=98=20Entity=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../woowacourse/touroot/travelogue/domain/Travelogue.java | 2 +- .../touroot/travelogueday/domain/TravelogueDay.java | 4 ++-- .../touroot/traveloguephoto/domain/TraveloguePhoto.java | 6 +++--- .../touroot/travelogueplace/domain/TraveloguePlace.java | 6 +++--- backend/src/main/resources/application.yml | 3 +++ 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java index 1899e848..bc6d0d8e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java @@ -25,5 +25,5 @@ public class Travelogue { private String thumbnail; @OneToMany(mappedBy = "travelogue") - private List days; + private List travelogueDays; } diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java b/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java index 2123b5b9..c39aad00 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java @@ -22,7 +22,7 @@ public class TravelogueDay { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(name = "DAY_ORDER", nullable = false) private Integer order; @JoinColumn(nullable = false) @@ -30,5 +30,5 @@ public class TravelogueDay { private Travelogue travelogue; @OneToMany(mappedBy = "travelogueDay") - private List places; + private List traveloguePlaces; } diff --git a/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java b/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java index 9bb79e4e..0ee66819 100644 --- a/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java +++ b/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java @@ -19,13 +19,13 @@ public class TraveloguePhoto { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(name = "PHOTO_KEY", nullable = false) private String key; - @Column(nullable = false) + @Column(name = "PHOTO_ORDER", nullable = false) private Integer order; @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) - private TraveloguePlace place; + private TraveloguePlace traveloguePlace; } diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java b/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java index 4b932fa2..9b17717c 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java +++ b/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java @@ -23,7 +23,7 @@ public class TraveloguePlace { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(name = "PLACE_ORDER", nullable = false) private Integer order; private String description; @@ -34,8 +34,8 @@ public class TraveloguePlace { @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) - private TravelogueDay day; + private TravelogueDay travelogueDay; @OneToMany(mappedBy = "traveloguePlace") - private List photos; + private List traveloguePhotos; } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 17164e13..aad6713f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,4 +1,7 @@ spring: + h2: + console: + enabled: true datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:test From f182fb504ed5bc4ffacf276b58d2edab0970060a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:43:47 +0900 Subject: [PATCH 012/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20Entity=20Repository=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../touroot/place/repository/PlaceRepository.java | 7 +++++++ .../travelogue/repository/TravelogueRepository.java | 7 +++++++ .../travelogueday/repository/TravelogueDayRepository.java | 7 +++++++ .../repository/TraveloguePhotoRepository.java | 7 +++++++ .../repsitory/TraveloguePlaceRepository.java | 7 +++++++ 5 files changed, 35 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java diff --git a/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java b/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java new file mode 100644 index 00000000..a3933b86 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.place.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.place.domain.Place; + +public interface PlaceRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java new file mode 100644 index 00000000..0b424122 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelogue.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelogue.domain.Travelogue; + +public interface TravelogueRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java new file mode 100644 index 00000000..99dce6fe --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelogueday.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelogueday.domain.TravelogueDay; + +public interface TravelogueDayRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java b/backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java new file mode 100644 index 00000000..9d2a06a5 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.traveloguephoto.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.traveloguephoto.domain.TraveloguePhoto; + +public interface TraveloguePhotoRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java new file mode 100644 index 00000000..59829ba7 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelogueplace.repsitory; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; + +public interface TraveloguePlaceRepository extends JpaRepository { +} From c971479228ad7c3cafb9ae1f34bd865928f82cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:09:42 +0900 Subject: [PATCH 013/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20Service?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 장소에 대한 위도, 경도의 타입을 Double에서 String 으로 변경 * feat: 여행기 상세 조회 응답 response 생성 * feat: 여행기 상세 조회 Service 생성 * refactor: 여행기 상세 조회 응답 구조 변경 --- .../touroot/place/domain/Place.java | 4 +- .../travelogue/dto/TravelogueResponse.java | 7 +++ .../travelogue/service/TravelogueService.java | 59 +++++++++++++++++++ .../dto/TravelogueDayResponse.java | 7 +++ .../dto/TraveloguePlaceResponse.java | 12 ++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java diff --git a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java index 25cb57f9..a263cdbb 100644 --- a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java +++ b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java @@ -19,10 +19,10 @@ public class Place { private String name; @Column(nullable = false) - private Double latitude; + private String latitude; @Column(nullable = false) - private Double longitude; + private String longitude; private String googlePlaceId; } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java new file mode 100644 index 00000000..effa2952 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelogue.dto; + +import java.util.List; +import woowacourse.touroot.travelogueday.dto.TravelogueDayResponse; + +public record TravelogueResponse(String title, String thumbnail, List days) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java new file mode 100644 index 00000000..e4bc95b9 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java @@ -0,0 +1,59 @@ +package woowacourse.touroot.travelogue.service; + +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.dto.TravelogueResponse; +import woowacourse.touroot.travelogue.repository.TravelogueRepository; +import woowacourse.touroot.travelogueday.domain.TravelogueDay; +import woowacourse.touroot.travelogueday.dto.TravelogueDayResponse; +import woowacourse.touroot.traveloguephoto.domain.TraveloguePhoto; +import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; +import woowacourse.touroot.travelogueplace.dto.TraveloguePlaceResponse; + +@RequiredArgsConstructor +@Service +public class TravelogueService { + + private final TravelogueRepository travelogueRepository; + + @Transactional(readOnly = true) + public TravelogueResponse findTravelogueById(Long id) { + Travelogue travelogue = travelogueRepository.findById(id).get(); + + return new TravelogueResponse(travelogue.getTitle(), travelogue.getThumbnail(), getDayResponses(travelogue)); + } + + private List getDayResponses(Travelogue travelogue) { + return travelogue.getTravelogueDays() + .stream() + .sorted(Comparator.comparing(TravelogueDay::getOrder)) + .map(travelogueDay -> new TravelogueDayResponse(getPlaceResponses(travelogueDay))) + .toList(); + } + + private List getPlaceResponses(TravelogueDay day) { + return day.getTraveloguePlaces() + .stream() + .sorted(Comparator.comparing(TraveloguePlace::getOrder)) + .map(traveloguePlace -> new TraveloguePlaceResponse( + traveloguePlace.getPlace().getName(), + getPhotoUrls(traveloguePlace), + traveloguePlace.getDescription(), + traveloguePlace.getPlace().getLatitude(), + traveloguePlace.getPlace().getLongitude() + )) + .toList(); + } + + private List getPhotoUrls(TraveloguePlace traveloguePlace) { + return traveloguePlace.getTraveloguePhotos() + .stream() + .sorted(Comparator.comparing(TraveloguePhoto::getOrder)) + .map(TraveloguePhoto::getKey) + .toList(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java new file mode 100644 index 00000000..69f25f67 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelogueday.dto; + +import java.util.List; +import woowacourse.touroot.travelogueplace.dto.TraveloguePlaceResponse; + +public record TravelogueDayResponse(List places) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java new file mode 100644 index 00000000..c746c90a --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java @@ -0,0 +1,12 @@ +package woowacourse.touroot.travelogueplace.dto; + +import java.util.List; + +public record TraveloguePlaceResponse( + String name, + List photoUrls, + String description, + String latitude, + String longitude +) { +} From 60b38f1894a448d17d142fb878479d0634beb40c Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:10:55 +0900 Subject: [PATCH 014/108] =?UTF-8?q?[Feature]=20-=20local/dev=20profile=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: dev profile 분리 * fix: username/password 환경 변수로 분리 --- backend/Dockerfile | 2 +- backend/src/main/resources/application.yml | 27 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 6958813d..b765de14 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ FROM openjdk:17-oracle ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "/app.jar"] diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index aad6713f..bedfde4e 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,16 +1,41 @@ spring: + config: + activate: + on-profile: local h2: console: enabled: true datasource: driver-class-name: org.h2.Driver - url: jdbc:h2:mem:test + url: jdbc:h2:mem:touroot username: sa jpa: show-sql: true properties: hibernate: format_sql: true + dialect: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop defer-datasource-initialization: true +--- +spring: + config: + activate: + on-profile: dev + h2: + console: + enabled: false + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: none From a154c710882f5298ca3cc3dbd23a4d2d0ea54bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:29:45 +0900 Subject: [PATCH 015/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20Controlle?= =?UTF-8?q?r=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TravelogueController.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java new file mode 100644 index 00000000..cb2d5a8c --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java @@ -0,0 +1,23 @@ +package woowacourse.touroot.travelogue.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import woowacourse.touroot.travelogue.dto.TravelogueResponse; +import woowacourse.touroot.travelogue.service.TravelogueService; + +@RequiredArgsConstructor +@RestController +@RequestMapping("api/v1/travelogues") +public class TravelogueController { + + private final TravelogueService travelogueService; + + @GetMapping("/{id}") + public ResponseEntity findTravelogue(@PathVariable Long id) { + return ResponseEntity.ok(travelogueService.findTravelogueById(id)); + } +} From 424284321bd71972ae6b1128508402a29fccaa32 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 16 Jul 2024 10:19:50 +0900 Subject: [PATCH 016/108] =?UTF-8?q?[Feature]=20-=20self=20hosted=20runner?= =?UTF-8?q?=20=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=9C=20CD=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Github Actions self-hosted runner를 활용한 CD 구축 * fix: be-dev-ci deploy 의존성 추가 --------- Co-authored-by: eunjungL --- .github/workflows/be-dev-ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/be-dev-ci.yml b/.github/workflows/be-dev-ci.yml index 12ba5209..336906d8 100644 --- a/.github/workflows/be-dev-ci.yml +++ b/.github/workflows/be-dev-ci.yml @@ -1,4 +1,4 @@ -name: BE-DEV-CI +name: BE DEV CI/CD on: push: @@ -41,3 +41,11 @@ jobs: - name: Push the Docker Image to Dockerhub run: docker push touroot/touroot-api + + deploy: + needs: build + runs-on: self-hosted + + steps: + - name: Pull the Docker Image and deploy + run: ../deploy.sh From 89042a70fdff7101287b264389071124f11a94b8 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 16 Jul 2024 10:32:37 +0900 Subject: [PATCH 017/108] =?UTF-8?q?[Fix]=20-=20CD=20deploy.sh=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/{be-dev-ci.yml => be-dev-ci-cd.yml} | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename .github/workflows/{be-dev-ci.yml => be-dev-ci-cd.yml} (82%) diff --git a/.github/workflows/be-dev-ci.yml b/.github/workflows/be-dev-ci-cd.yml similarity index 82% rename from .github/workflows/be-dev-ci.yml rename to .github/workflows/be-dev-ci-cd.yml index 336906d8..5bf3ced5 100644 --- a/.github/workflows/be-dev-ci.yml +++ b/.github/workflows/be-dev-ci-cd.yml @@ -47,5 +47,11 @@ jobs: runs-on: self-hosted steps: - - name: Pull the Docker Image and deploy - run: ../deploy.sh + - name: Docker Image pull + run: sudo docker pull touroot/touroot-api + + - name: Move to Docker directory + run: cd ~/docker + + - name: Docker Compose up + run: sudo docker compose -f touroot-docker.yml up -d From d688b56d9ba3c25d9086dbbde964f97e363ad8f5 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 16 Jul 2024 10:52:05 +0900 Subject: [PATCH 018/108] =?UTF-8?q?[Fix]=20-=20CD=20docker=20compose=20up?= =?UTF-8?q?=20=EC=88=98=ED=96=89=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/be-dev-ci-cd.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/be-dev-ci-cd.yml b/.github/workflows/be-dev-ci-cd.yml index 5bf3ced5..0662ccfb 100644 --- a/.github/workflows/be-dev-ci-cd.yml +++ b/.github/workflows/be-dev-ci-cd.yml @@ -50,8 +50,5 @@ jobs: - name: Docker Image pull run: sudo docker pull touroot/touroot-api - - name: Move to Docker directory - run: cd ~/docker - - name: Docker Compose up - run: sudo docker compose -f touroot-docker.yml up -d + run: sudo docker compose -f ~/docker/touroot-docker.yml up -d From 74ee4c2e393f763ad91a19170b6913b9f481fe24 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 16 Jul 2024 10:56:06 +0900 Subject: [PATCH 019/108] =?UTF-8?q?[Feature]=20-=20CORS=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../touroot/global/config/WebConfig.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java new file mode 100644 index 00000000..5715522c --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java @@ -0,0 +1,15 @@ +package woowacourse.touroot.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000"); + } +} From 7a7c5dc655218a995ad6d2830ecd26dfb845a1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:14:06 +0900 Subject: [PATCH 020/108] =?UTF-8?q?[Feature]=20-=20PR=EC=9D=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=95=A0=20=EB=95=8C=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=98=EB=8A=94=20?= =?UTF-8?q?CI=20Workflow=20=EC=B6=94=EA=B0=80=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: pull request workflow 생성 * refactor: github action workflow yml 파일 이름 변경 - be-pull-request.yml -> be-ci.yml - be-dev-ci-cd.yml -> be-cd-dev.yml --- .../{be-dev-ci-cd.yml => be-cd-dev.yml} | 2 +- .github/workflows/be-ci.yml | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) rename .github/workflows/{be-dev-ci-cd.yml => be-cd-dev.yml} (98%) create mode 100644 .github/workflows/be-ci.yml diff --git a/.github/workflows/be-dev-ci-cd.yml b/.github/workflows/be-cd-dev.yml similarity index 98% rename from .github/workflows/be-dev-ci-cd.yml rename to .github/workflows/be-cd-dev.yml index 0662ccfb..8a8bd27f 100644 --- a/.github/workflows/be-dev-ci-cd.yml +++ b/.github/workflows/be-cd-dev.yml @@ -1,4 +1,4 @@ -name: BE DEV CI/CD +name: BE CD DEV on: push: diff --git a/.github/workflows/be-ci.yml b/.github/workflows/be-ci.yml new file mode 100644 index 00000000..05ac48a5 --- /dev/null +++ b/.github/workflows/be-ci.yml @@ -0,0 +1,41 @@ +name: BE CI + +on: + pull_request: + branches: [ production/be, develop/be ] + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test with Gradle + run: ./gradlew build + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + if: ${{ always() }} + with: + files: ./build/test-results/**/*.xml + + - name: When test fail, comment on that code + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: ./build/test-results/**/*.xml + token: ${{ github.token }} From 1c0aa6291ad82799878fbb6cb6103bd010c7e3ab Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 17 Jul 2024 14:55:06 +0900 Subject: [PATCH 021/108] =?UTF-8?q?[Feature]=20-=20JPA=20Auditing=EC=9D=84?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20BaseEntity=20=EC=83=9D=EC=84=B1=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 패키지 구조 변경 * feat: JPA Auditing을 위한 BaseEntity 추가 * refactor: BaseEntity 추상 클래스화 및 코드 컨벤션 준수 --- .../touroot/TourootApplication.java | 9 ++++---- .../touroot/entity/BaseEntity.java | 23 +++++++++++++++++++ .../touroot/place/domain/Place.java | 3 ++- .../day}/domain/TravelogueDay.java | 7 +++--- .../day/dto/TravelogueDayResponse.java | 7 ++++++ .../repository/TravelogueDayRepository.java | 4 ++-- .../touroot/travelogue/domain/Travelogue.java | 5 ++-- .../travelogue/dto/TravelogueResponse.java | 2 +- .../photo}/domain/TraveloguePhoto.java | 7 +++--- .../repository/TraveloguePhotoRepository.java | 4 ++-- .../place}/domain/TraveloguePlace.java | 9 ++++---- .../place}/dto/TraveloguePlaceResponse.java | 2 +- .../repsitory/TraveloguePlaceRepository.java | 4 ++-- .../travelogue/service/TravelogueService.java | 10 ++++---- .../dto/TravelogueDayResponse.java | 7 ------ 15 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java rename backend/src/main/java/woowacourse/touroot/{travelogueday => travelogue/day}/domain/TravelogueDay.java (79%) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java rename backend/src/main/java/woowacourse/touroot/{travelogueday => travelogue/day}/repository/TravelogueDayRepository.java (56%) rename backend/src/main/java/woowacourse/touroot/{traveloguephoto => travelogue/photo}/domain/TraveloguePhoto.java (76%) rename backend/src/main/java/woowacourse/touroot/{traveloguephoto => travelogue/photo}/repository/TraveloguePhotoRepository.java (55%) rename backend/src/main/java/woowacourse/touroot/{travelogueplace => travelogue/place}/domain/TraveloguePlace.java (77%) rename backend/src/main/java/woowacourse/touroot/{travelogueplace => travelogue/place}/dto/TraveloguePlaceResponse.java (80%) rename backend/src/main/java/woowacourse/touroot/{travelogueplace => travelogue/place}/repsitory/TraveloguePlaceRepository.java (55%) delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java diff --git a/backend/src/main/java/woowacourse/touroot/TourootApplication.java b/backend/src/main/java/woowacourse/touroot/TourootApplication.java index a3f26375..1106428e 100644 --- a/backend/src/main/java/woowacourse/touroot/TourootApplication.java +++ b/backend/src/main/java/woowacourse/touroot/TourootApplication.java @@ -2,12 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class TourootApplication { - public static void main(String[] args) { - SpringApplication.run(TourootApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(TourootApplication.class, args); + } } diff --git a/backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java b/backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java new file mode 100644 index 00000000..bb3527a6 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java @@ -0,0 +1,23 @@ +package woowacourse.touroot.entity; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime modifiedAt; + + private LocalDateTime deletedAt; +} diff --git a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java index a263cdbb..5509ca4c 100644 --- a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java +++ b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java @@ -6,10 +6,11 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Getter; +import woowacourse.touroot.entity.BaseEntity; @Getter @Entity -public class Place { +public class Place extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java b/backend/src/main/java/woowacourse/touroot/travelogue/day/domain/TravelogueDay.java similarity index 79% rename from backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java rename to backend/src/main/java/woowacourse/touroot/travelogue/day/domain/TravelogueDay.java index c39aad00..d9d36580 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueday/domain/TravelogueDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/day/domain/TravelogueDay.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogueday.domain; +package woowacourse.touroot.travelogue.day.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -11,12 +11,13 @@ import jakarta.persistence.OneToMany; import java.util.List; import lombok.Getter; +import woowacourse.touroot.entity.BaseEntity; import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; @Getter @Entity -public class TravelogueDay { +public class TravelogueDay extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java new file mode 100644 index 00000000..ca111591 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelogue.day.dto; + +import java.util.List; +import woowacourse.touroot.travelogue.place.dto.TraveloguePlaceResponse; + +public record TravelogueDayResponse(List places) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java similarity index 56% rename from backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java rename to backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java index 99dce6fe..a4e3c65d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueday/repository/TravelogueDayRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java @@ -1,7 +1,7 @@ -package woowacourse.touroot.travelogueday.repository; +package woowacourse.touroot.travelogue.day.repository; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogueday.domain.TravelogueDay; +import woowacourse.touroot.travelogue.day.domain.TravelogueDay; public interface TravelogueDayRepository extends JpaRepository { } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java index bc6d0d8e..d3d0029c 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java @@ -8,11 +8,12 @@ import jakarta.persistence.OneToMany; import java.util.List; import lombok.Getter; -import woowacourse.touroot.travelogueday.domain.TravelogueDay; +import woowacourse.touroot.entity.BaseEntity; +import woowacourse.touroot.travelogue.day.domain.TravelogueDay; @Getter @Entity -public class Travelogue { +public class Travelogue extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java index effa2952..bd669104 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java @@ -1,7 +1,7 @@ package woowacourse.touroot.travelogue.dto; import java.util.List; -import woowacourse.touroot.travelogueday.dto.TravelogueDayResponse; +import woowacourse.touroot.travelogue.day.dto.TravelogueDayResponse; public record TravelogueResponse(String title, String thumbnail, List days) { } diff --git a/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java b/backend/src/main/java/woowacourse/touroot/travelogue/photo/domain/TraveloguePhoto.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java rename to backend/src/main/java/woowacourse/touroot/travelogue/photo/domain/TraveloguePhoto.java index 0ee66819..1c32f522 100644 --- a/backend/src/main/java/woowacourse/touroot/traveloguephoto/domain/TraveloguePhoto.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/photo/domain/TraveloguePhoto.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.traveloguephoto.domain; +package woowacourse.touroot.travelogue.photo.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -9,11 +9,12 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import lombok.Getter; -import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; +import woowacourse.touroot.entity.BaseEntity; +import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; @Getter @Entity -public class TraveloguePhoto { +public class TraveloguePhoto extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java similarity index 55% rename from backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java rename to backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java index 9d2a06a5..e492080f 100644 --- a/backend/src/main/java/woowacourse/touroot/traveloguephoto/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java @@ -1,7 +1,7 @@ -package woowacourse.touroot.traveloguephoto.repository; +package woowacourse.touroot.travelogue.photo.repository; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.traveloguephoto.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.photo.domain.TraveloguePhoto; public interface TraveloguePhotoRepository extends JpaRepository { } diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java b/backend/src/main/java/woowacourse/touroot/travelogue/place/domain/TraveloguePlace.java similarity index 77% rename from backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java rename to backend/src/main/java/woowacourse/touroot/travelogue/place/domain/TraveloguePlace.java index 9b17717c..a4bd7a6d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueplace/domain/TraveloguePlace.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/place/domain/TraveloguePlace.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogueplace.domain; +package woowacourse.touroot.travelogue.place.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -11,13 +11,14 @@ import jakarta.persistence.OneToMany; import java.util.List; import lombok.Getter; +import woowacourse.touroot.entity.BaseEntity; import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogueday.domain.TravelogueDay; -import woowacourse.touroot.traveloguephoto.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.photo.domain.TraveloguePhoto; @Getter @Entity -public class TraveloguePlace { +public class TraveloguePlace extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java similarity index 80% rename from backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java rename to backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java index c746c90a..349c784d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueplace/dto/TraveloguePlaceResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogueplace.dto; +package woowacourse.touroot.travelogue.place.dto; import java.util.List; diff --git a/backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java similarity index 55% rename from backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java rename to backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java index 59829ba7..3395e41b 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogueplace/repsitory/TraveloguePlaceRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java @@ -1,7 +1,7 @@ -package woowacourse.touroot.travelogueplace.repsitory; +package woowacourse.touroot.travelogue.place.repsitory; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; public interface TraveloguePlaceRepository extends JpaRepository { } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java index e4bc95b9..fc1a7ed8 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java @@ -5,14 +5,14 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.travelogue.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.day.dto.TravelogueDayResponse; import woowacourse.touroot.travelogue.domain.Travelogue; import woowacourse.touroot.travelogue.dto.TravelogueResponse; +import woowacourse.touroot.travelogue.photo.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.place.dto.TraveloguePlaceResponse; import woowacourse.touroot.travelogue.repository.TravelogueRepository; -import woowacourse.touroot.travelogueday.domain.TravelogueDay; -import woowacourse.touroot.travelogueday.dto.TravelogueDayResponse; -import woowacourse.touroot.traveloguephoto.domain.TraveloguePhoto; -import woowacourse.touroot.travelogueplace.domain.TraveloguePlace; -import woowacourse.touroot.travelogueplace.dto.TraveloguePlaceResponse; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java deleted file mode 100644 index 69f25f67..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogueday/dto/TravelogueDayResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.touroot.travelogueday.dto; - -import java.util.List; -import woowacourse.touroot.travelogueplace.dto.TraveloguePlaceResponse; - -public record TravelogueDayResponse(List places) { -} From 2f60adb21582482e0bc8f806dcecbe760af29f22 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:19:37 +0900 Subject: [PATCH 022/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=9E=91=EC=84=B1=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: TravelPlan 관련 Entity 추가 * feat: 여행기 작성 API 구현 * feat: swagger 설명 추가 * feat: 여행 계획 작성 시 지난 날짜에 대한 검증 추가 * refactor: TravelPlanService method 분리 * refactor: PlanPlaceRequest에 toPlace 추가 * style: TravelPlanControllere 개행 정리 --- .../global/exception/BadRequestException.java | 8 +++ .../touroot/place/domain/Place.java | 15 +++-- .../place/repository/PlaceRepository.java | 4 ++ .../controller/TravelPlanController.java | 31 ++++++++++ .../touroot/travelplan/domain/TravelPlan.java | 42 +++++++++++++ .../travelplan/domain/TravelPlanDay.java | 35 +++++++++++ .../travelplan/domain/TravelPlanPlace.java | 37 +++++++++++ .../travelplan/dto/PlanDayCreateRequest.java | 23 +++++++ .../dto/PlanLocationCreateRequest.java | 14 +++++ .../dto/PlanPlaceCreateRequest.java | 30 +++++++++ .../dto/TravelPlanCreateRequest.java | 26 ++++++++ .../dto/TravelPlanCreateResponse.java | 9 +++ .../repository/TravelPlanDayRepository.java | 7 +++ .../repository/TravelPlanPlaceRepository.java | 7 +++ .../repository/TravelPlanRepository.java | 7 +++ .../travelplan/service/TravelPlanService.java | 61 +++++++++++++++++++ .../travelplan/domain/TravelPlanTest.java | 25 ++++++++ 17 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java b/backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java new file mode 100644 index 00000000..94306533 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package woowacourse.touroot.global.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java index 5509ca4c..630c3a94 100644 --- a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java +++ b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java @@ -1,14 +1,15 @@ package woowacourse.touroot.place.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class Place extends BaseEntity { @@ -26,4 +27,8 @@ public class Place extends BaseEntity { private String longitude; private String googlePlaceId; + + public Place(String name, String latitude, String longitude) { + this(null, name, latitude, longitude, null); + } } diff --git a/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java b/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java index a3933b86..4b0c8ce6 100644 --- a/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java +++ b/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java @@ -3,5 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import woowacourse.touroot.place.domain.Place; +import java.util.Optional; + public interface PlaceRepository extends JpaRepository { + + Optional findByNameAndLatitudeAndLongitude(String name, String lat, String lng); } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java new file mode 100644 index 00000000..de03821f --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java @@ -0,0 +1,31 @@ +package woowacourse.touroot.travelplan.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.service.TravelPlanService; + +@Tag(name = "여행기") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/travel-plans") +public class TravelPlanController { + + private final TravelPlanService travelPlanService; + + @Operation(summary = "여행기 생성") + @PostMapping + public ResponseEntity createTravelPlan(@Valid @RequestBody TravelPlanCreateRequest request) { + TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request); + return ResponseEntity.ok() + .body(data); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java new file mode 100644 index 00000000..4699b2bc --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java @@ -0,0 +1,42 @@ +package woowacourse.touroot.travelplan.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import woowacourse.touroot.entity.BaseEntity; +import woowacourse.touroot.global.exception.BadRequestException; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class TravelPlan extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private LocalDate startDate; + + @OneToMany(mappedBy = "plan") + private List days; + + public TravelPlan(String title, LocalDate startDate) { + this(null, title, startDate, null); + } + + public void validateStartDate() { + if (startDate.isBefore(LocalDate.now())) { + throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); + } + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java new file mode 100644 index 00000000..fe08d0d3 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java @@ -0,0 +1,35 @@ +package woowacourse.touroot.travelplan.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import woowacourse.touroot.entity.BaseEntity; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class TravelPlanDay extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "plan_day_order", nullable = false) + Integer order; + + @JoinColumn(name = "plan_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private TravelPlan plan; + + @OneToMany(mappedBy = "day") + private List places; + + public TravelPlanDay(int order, TravelPlan plan) { + this(null, order, plan, null); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java new file mode 100644 index 00000000..71733dad --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java @@ -0,0 +1,37 @@ +package woowacourse.touroot.travelplan.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import woowacourse.touroot.entity.BaseEntity; +import woowacourse.touroot.place.domain.Place; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class TravelPlanPlace extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String description; + + @Column(name = "plan_place_order", nullable = false) + private Integer order; + + @JoinColumn(name = "plan_day_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private TravelPlanDay day; + + @JoinColumn(name = "place_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Place place; + + public TravelPlanPlace(String description, int order, TravelPlanDay day, Place place) { + this(null, description, order, day, place); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java new file mode 100644 index 00000000..99aedbca --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java @@ -0,0 +1,23 @@ +package woowacourse.touroot.travelplan.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import woowacourse.touroot.travelplan.domain.TravelPlan; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; + +import java.util.List; + +public record PlanDayCreateRequest( + @Schema(description = "여행 계획 날짜", example = "1") + @NotNull(message = "날짜는 비어있을 수 없습니다.") + @Min(value = 0, message = "날짜는 1 이상이어야 합니다.") + int day, + @Schema(description = "여행 장소 정보") + @NotNull(message = "여행 장소 정보는 비어있을 수 없습니다.") List places +) { + + public TravelPlanDay toPlanDay(TravelPlan plan) { + return new TravelPlanDay(day, plan); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java new file mode 100644 index 00000000..5ba1bd42 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java @@ -0,0 +1,14 @@ +package woowacourse.touroot.travelplan.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record PlanLocationCreateRequest( + @Schema(description = "여행 장소 위도", example = "37.5175896") + @NotNull(message = "위도는 비어있을 수 없습니다.") + String lat, + @Schema(description = "여행 장소 경도", example = "127.0867236") + @NotNull(message = "경도는 비어있을 수 없습니다.") + String lng +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java new file mode 100644 index 00000000..182cf88b --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java @@ -0,0 +1,30 @@ +package woowacourse.touroot.travelplan.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; + +public record PlanPlaceCreateRequest( + @Schema(description = "여행 장소 이름", example = "신나는 여행 장소") + @NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName, + @Schema(description = "여행 장소 설명", example = "잠실한강공원") + String description, + @Schema(description = "여행 장소 순서", example = "1") + @NotNull + @Min(value = 0, message = "순서는 1 이상이어야 합니다.") + int order, + @NotNull PlanLocationCreateRequest location +) { + + public TravelPlanPlace toPlanPlace(TravelPlanDay day, Place place) { + return new TravelPlanPlace(description, order, day, place); + } + + public Place toPlace() { + return new Place(placeName, location.lat(), location.lng()); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java new file mode 100644 index 00000000..e88e2fce --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java @@ -0,0 +1,26 @@ +package woowacourse.touroot.travelplan.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import woowacourse.touroot.travelplan.domain.TravelPlan; + +import java.time.LocalDate; +import java.util.List; + +public record TravelPlanCreateRequest( + @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") + @NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.") + String title, + @Schema(description = "여행 계획 시작일", example = "2024-11-16") + @NotNull(message = "시작일은 비어있을 수 없습니다.") + LocalDate startDate, + @Schema(description = "여행 날짜 정보") + @NotNull(message = "여행 날짜 정보는 비어있을 수 없습니다.") + List days +) { + + public TravelPlan toTravelPlan() { + return new TravelPlan(title, startDate); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java new file mode 100644 index 00000000..26b147be --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java @@ -0,0 +1,9 @@ +package woowacourse.touroot.travelplan.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record TravelPlanCreateResponse( + @Schema(description = "생성된 여행 계획 id") + Long id +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java new file mode 100644 index 00000000..18bf271f --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelplan.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; + +public interface TravelPlanDayRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java new file mode 100644 index 00000000..66d04742 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelplan.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; + +public interface TravelPlanPlaceRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java new file mode 100644 index 00000000..0665c7a6 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java @@ -0,0 +1,7 @@ +package woowacourse.touroot.travelplan.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelplan.domain.TravelPlan; + +public interface TravelPlanRepository extends JpaRepository { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java new file mode 100644 index 00000000..f962ea03 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java @@ -0,0 +1,61 @@ +package woowacourse.touroot.travelplan.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.place.repository.PlaceRepository; +import woowacourse.touroot.travelplan.domain.TravelPlan; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; +import woowacourse.touroot.travelplan.dto.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; +import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; +import woowacourse.touroot.travelplan.repository.TravelPlanRepository; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class TravelPlanService { + + private final TravelPlanRepository travelPlanRepository; + private final TravelPlanDayRepository travelPlanDayRepository; + private final TravelPlanPlaceRepository travelPlanPlaceRepository; + private final PlaceRepository placeRepository; + + @Transactional + public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request) { + TravelPlan travelPlan = request.toTravelPlan(); + travelPlan.validateStartDate(); + + TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); + createPlanDay(request, savedTravelPlan); + + return new TravelPlanCreateResponse(savedTravelPlan.getId()); + } + + private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) { + for (PlanDayCreateRequest dayRequest : request.days()) { + TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan)); + createPlanPlace(dayRequest.places(), travelPlanDay); + } + } + + private void createPlanPlace(List request, TravelPlanDay travelPlanDay) { + for (PlanPlaceCreateRequest planRequest : request) { + Place place = getPlace(planRequest); + travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place)); + } + } + + private Place getPlace(PlanPlaceCreateRequest planRequest) { + return placeRepository.findByNameAndLatitudeAndLongitude( + planRequest.placeName(), + planRequest.location().lat(), + planRequest.location().lng() + ).orElseGet(() -> placeRepository.save(planRequest.toPlace())); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java new file mode 100644 index 00000000..0548a6a6 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelplan.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import woowacourse.touroot.global.exception.BadRequestException; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThatCode; + +@DisplayName("여행 계획") +class TravelPlanTest { + + @DisplayName("여행 계획은 지난 날짜를 검증할 시 예외가 발생한다.") + @Test + void validateStartDate() { + // given + TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN); + + // when & then + assertThatCode(travelPlan::validateStartDate) + .isInstanceOf(BadRequestException.class) + .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); + } +} From 63ca1ef9c95d985b9b3aa173ccc812a4088a9a28 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Thu, 18 Jul 2024 17:03:23 +0900 Subject: [PATCH 023/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 패키지 구조 변경 * feat: 여행기 상세 조회 로직 리팩토링 - 각 엔티티 별 Service 계층으로 역할 위임 * refactor: 여행기 상세 조회 로직에 Facade 패턴 적용 * feat: 통합 테스트를 위한 RestAssured 의존성 추가 * feat: 테스트 Profile 추가 * test: 통합 테스트를 위한 유틸 클래스 작성 * refactor: 영속화 이전의 엔티티 객체 생성을 위한 생성자 추가 * test: 여행기 상세 조회 API 통합 테스트 작성 * refactor: DTO Bean Validation 추가 * refactor: Test profile 활성화 조건 추가 * fix: application.yml EOL 추가 * refactor: 코드 컨벤션에 맞춰 공백 라인 추가 * refactor: 커스텀 예외를 활용하도록 리팩토링 * refactor: Facade 클래스의 계층을 클래스명에 명시 * refactor: 전역 사용 커스텀 어노테이션 패키지 경로 변경 * refactor: Facade 패턴에 더 알맞게 각 서비스 클래스의 역할 분리 --- backend/build.gradle | 47 ++++++------ .../touroot/place/domain/Place.java | 4 + .../controller/TravelogueController.java | 13 +++- .../day/dto/TravelogueDayResponse.java | 7 -- .../repository/TravelogueDayRepository.java | 7 -- .../touroot/travelogue/domain/Travelogue.java | 13 ++-- .../day/domain/TravelogueDay.java | 15 ++-- .../domain/day/dto/TravelogueDayResponse.java | 18 +++++ .../repository/TravelogueDayRepository.java | 11 +++ .../day/service/TravelogueDayService.java | 28 +++++++ .../photo/domain/TraveloguePhoto.java | 13 +++- .../repository/TraveloguePhotoRepository.java | 11 +++ .../photo/service/TraveloguePhotoService.java | 25 ++++++ .../place/domain/TraveloguePlace.java | 29 +++++-- .../place/dto/TraveloguePlaceResponse.java | 28 +++++++ .../repsitory/TraveloguePlaceRepository.java | 11 +++ .../place/service/TraveloguePlaceService.java | 28 +++++++ .../travelogue/dto/TravelogueResponse.java | 21 ++++- .../repository/TraveloguePhotoRepository.java | 7 -- .../place/dto/TraveloguePlaceResponse.java | 12 --- .../repsitory/TraveloguePlaceRepository.java | 7 -- .../service/TravelogueFacadeService.java | 76 +++++++++++++++++++ .../travelogue/service/TravelogueService.java | 46 +---------- backend/src/main/resources/application.yml | 24 ++++++ .../touroot/global/AcceptanceTest.java | 13 ++++ .../controller/TravelogueControllerTest.java | 46 +++++++++++ .../touroot/utils/DatabaseCleaner.java | 74 ++++++++++++++++++ .../touroot/utils/TestFixture.java | 70 +++++++++++++++++ 28 files changed, 573 insertions(+), 131 deletions(-) delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java rename backend/src/main/java/woowacourse/touroot/travelogue/{ => domain}/day/domain/TravelogueDay.java (67%) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java rename backend/src/main/java/woowacourse/touroot/travelogue/{ => domain}/photo/domain/TraveloguePhoto.java (62%) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java rename backend/src/main/java/woowacourse/touroot/travelogue/{ => domain}/place/domain/TraveloguePlace.java (55%) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java create mode 100644 backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java create mode 100644 backend/src/test/java/woowacourse/touroot/utils/TestFixture.java diff --git a/backend/build.gradle b/backend/build.gradle index a8d8e817..c631365e 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,43 +1,44 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.3.1' - id 'io.spring.dependency-management' version '1.1.5' + id 'java' + id 'org.springframework.boot' version '3.3.1' + id 'io.spring.dependency-management' version '1.1.5' } group = 'woowacourse' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' - - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured:5.5.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java index 630c3a94..555d74cc 100644 --- a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java +++ b/backend/src/main/java/woowacourse/touroot/place/domain/Place.java @@ -28,6 +28,10 @@ public class Place extends BaseEntity { private String googlePlaceId; + public Place(String name, String latitude, String longitude, String googlePlaceId) { + this(null, name, latitude, longitude, googlePlaceId); + } + public Place(String name, String latitude, String longitude) { this(null, name, latitude, longitude, null); } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java index cb2d5a8c..41792d9d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java @@ -1,5 +1,8 @@ package woowacourse.touroot.travelogue.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -7,17 +10,19 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import woowacourse.touroot.travelogue.dto.TravelogueResponse; -import woowacourse.touroot.travelogue.service.TravelogueService; +import woowacourse.touroot.travelogue.service.TravelogueFacadeService; +@Tag(name = "여행기") @RequiredArgsConstructor @RestController @RequestMapping("api/v1/travelogues") public class TravelogueController { - private final TravelogueService travelogueService; + private final TravelogueFacadeService travelogueFacadeService; + @Operation(description = "여행기 상세 조회") @GetMapping("/{id}") - public ResponseEntity findTravelogue(@PathVariable Long id) { - return ResponseEntity.ok(travelogueService.findTravelogueById(id)); + public ResponseEntity findTravelogue(@Valid @PathVariable Long id) { + return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id)); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java deleted file mode 100644 index ca111591..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/day/dto/TravelogueDayResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.touroot.travelogue.day.dto; - -import java.util.List; -import woowacourse.touroot.travelogue.place.dto.TraveloguePlaceResponse; - -public record TravelogueDayResponse(List places) { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java deleted file mode 100644 index a4e3c65d..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/day/repository/TravelogueDayRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.touroot.travelogue.day.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.day.domain.TravelogueDay; - -public interface TravelogueDayRepository extends JpaRepository { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java index d3d0029c..045a53d2 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java @@ -5,13 +5,15 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.travelogue.day.domain.TravelogueDay; @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class Travelogue extends BaseEntity { @@ -25,6 +27,7 @@ public class Travelogue extends BaseEntity { @Column(nullable = false) private String thumbnail; - @OneToMany(mappedBy = "travelogue") - private List travelogueDays; + public Travelogue(String title, String thumbnail) { + this(null, title, thumbnail); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/day/domain/TravelogueDay.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/domain/TravelogueDay.java similarity index 67% rename from backend/src/main/java/woowacourse/touroot/travelogue/day/domain/TravelogueDay.java rename to backend/src/main/java/woowacourse/touroot/travelogue/domain/day/domain/TravelogueDay.java index d9d36580..e3d306e9 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/day/domain/TravelogueDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/domain/TravelogueDay.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.day.domain; +package woowacourse.touroot.travelogue.domain.day.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,14 +8,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TravelogueDay extends BaseEntity { @@ -30,6 +32,7 @@ public class TravelogueDay extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private Travelogue travelogue; - @OneToMany(mappedBy = "travelogueDay") - private List traveloguePlaces; + public TravelogueDay(Integer order, Travelogue travelogue) { + this(null, order, travelogue); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java new file mode 100644 index 00000000..15fbd212 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java @@ -0,0 +1,18 @@ +package woowacourse.touroot.travelogue.domain.day.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.Builder; +import woowacourse.touroot.travelogue.domain.place.dto.TraveloguePlaceResponse; + +@Builder +public record TravelogueDayResponse( + @Schema(description = "여행기 일자 ID", example = "1") + @NotNull(message = "ID는 비어있을 수 없습니다.") + Long id, + @Schema(description = "여행기 장소 목록") + @NotNull(message = "여행기 장소 정보는 비어있을 수 없습니다.") + List places +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java new file mode 100644 index 00000000..d480bf83 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java @@ -0,0 +1,11 @@ +package woowacourse.touroot.travelogue.domain.day.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; + +public interface TravelogueDayRepository extends JpaRepository { + + List findByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java new file mode 100644 index 00000000..93a99bbd --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java @@ -0,0 +1,28 @@ +package woowacourse.touroot.travelogue.domain.day.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.day.repository.TravelogueDayRepository; + +@RequiredArgsConstructor +@Service +public class TravelogueDayService { + + private final TravelogueDayRepository travelogueDayRepository; + + @Transactional(readOnly = true) + public List findDaysByTravelogue(Travelogue travelogue) { + return travelogueDayRepository.findByTravelogue(travelogue); + } + + @Transactional(readOnly = true) + public TravelogueDay findDayById(Long id) { + return travelogueDayRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 일자입니다.")); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/photo/domain/TraveloguePhoto.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/domain/TraveloguePhoto.java similarity index 62% rename from backend/src/main/java/woowacourse/touroot/travelogue/photo/domain/TraveloguePhoto.java rename to backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/domain/TraveloguePhoto.java index 1c32f522..bdc59bd6 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/photo/domain/TraveloguePhoto.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/domain/TraveloguePhoto.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.photo.domain; +package woowacourse.touroot.travelogue.domain.photo.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,11 +8,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TraveloguePhoto extends BaseEntity { @@ -29,4 +34,8 @@ public class TraveloguePhoto extends BaseEntity { @JoinColumn(nullable = false) @ManyToOne(fetch = FetchType.LAZY) private TraveloguePlace traveloguePlace; + + public TraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { + this(null, key, order, traveloguePlace); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java new file mode 100644 index 00000000..a63e83a3 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java @@ -0,0 +1,11 @@ +package woowacourse.touroot.travelogue.domain.photo.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelogue.domain.photo.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; + +public interface TraveloguePhotoRepository extends JpaRepository { + + List findByTraveloguePlace(TraveloguePlace traveloguePlace); +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java new file mode 100644 index 00000000..eb082eb8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelogue.domain.photo.service; + +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import woowacourse.touroot.travelogue.domain.photo.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.photo.repository.TraveloguePhotoRepository; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; + +@RequiredArgsConstructor +@Service +public class TraveloguePhotoService { + + private final TraveloguePhotoRepository traveloguePhotoRepository; + + public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { + List photos = traveloguePhotoRepository.findByTraveloguePlace(traveloguePlace); + + return photos.stream() + .sorted(Comparator.comparing(TraveloguePhoto::getOrder)) + .map(TraveloguePhoto::getKey) + .toList(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/place/domain/TraveloguePlace.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/domain/TraveloguePlace.java similarity index 55% rename from backend/src/main/java/woowacourse/touroot/travelogue/place/domain/TraveloguePlace.java rename to backend/src/main/java/woowacourse/touroot/travelogue/domain/place/domain/TraveloguePlace.java index a4bd7a6d..21942047 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/place/domain/TraveloguePlace.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/domain/TraveloguePlace.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.place.domain; +package woowacourse.touroot.travelogue.domain.place.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,15 +8,17 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.photo.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TraveloguePlace extends BaseEntity { @@ -37,6 +39,19 @@ public class TraveloguePlace extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private TravelogueDay travelogueDay; - @OneToMany(mappedBy = "traveloguePlace") - private List traveloguePhotos; + public TraveloguePlace(Integer order, String description, Place place, TravelogueDay travelogueDay) { + this(null, order, description, place, travelogueDay); + } + + public String getName() { + return place.getName(); + } + + public String getLatitude() { + return place.getLatitude(); + } + + public String getLongitude() { + return place.getLongitude(); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java new file mode 100644 index 00000000..d89220af --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java @@ -0,0 +1,28 @@ +package woowacourse.touroot.travelogue.domain.place.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.Builder; + +@Builder +public record TraveloguePlaceResponse( + @Schema(description = "여행기 장소 ID", example = "1") + @NotNull(message = "ID는 비어있을 수 없습니다.") + Long id, + @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") + @NotBlank(message = "여행기 장소 이름은 비어있을 수 없습니다.") + String name, + @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") + @NotBlank(message = "여행기 장소 설명은 비어있을 수 없습니다.") + String description, + @Schema(description = "여행기 장소 위도", example = "37.5175896") + @NotBlank(message = "여행기 장소 위도는 비어있을 수 없습니다.") + String lat, + @Schema(description = "여행기 장소 설명", example = "127.0867236") + @NotBlank(message = "여행기 장소 경도는 비어있을 수 없습니다.") + String lng, + List photoUrls +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java new file mode 100644 index 00000000..d6e886c4 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java @@ -0,0 +1,11 @@ +package woowacourse.touroot.travelogue.domain.place.repsitory; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; + +public interface TraveloguePlaceRepository extends JpaRepository { + + List findByTravelogueDay(TravelogueDay travelogueDay); +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java new file mode 100644 index 00000000..9a6694de --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java @@ -0,0 +1,28 @@ +package woowacourse.touroot.travelogue.domain.place.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.domain.place.repsitory.TraveloguePlaceRepository; + +@RequiredArgsConstructor +@Service +public class TraveloguePlaceService { + + private final TraveloguePlaceRepository traveloguePlaceRepository; + + @Transactional(readOnly = true) + public List findTraveloguePlaceByDay(TravelogueDay travelogueDay) { + return traveloguePlaceRepository.findByTravelogueDay(travelogueDay); + } + + @Transactional(readOnly = true) + public TraveloguePlace findTraveloguePlaceById(Long id) { + return traveloguePlaceRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 장소입니다.")); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java index bd669104..e9f44219 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java @@ -1,7 +1,24 @@ package woowacourse.touroot.travelogue.dto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import java.util.List; -import woowacourse.touroot.travelogue.day.dto.TravelogueDayResponse; +import lombok.Builder; +import woowacourse.touroot.travelogue.domain.day.dto.TravelogueDayResponse; -public record TravelogueResponse(String title, String thumbnail, List days) { +@Builder +public record TravelogueResponse( + @Schema(description = "여행기 ID", example = "1") + @NotNull(message = "ID는 비어있을 수 없습니다.") + Long id, + @Schema(description = "여행기 제목", example = "서울 강남 여행기") + @NotNull(message = "여행기 제목은 비어있을 수 없습니다.") + String title, + @Schema(description = "여행기 섬네일 링크", example = "https://섬네일.png") + @NotNull(message = "여행기 섬네일 링크는 비어있을 수 없습니다.") + String thumbnail, + @Schema(description = "여행기 일자 목록") + @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") + List days +) { } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java deleted file mode 100644 index e492080f..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/photo/repository/TraveloguePhotoRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.touroot.travelogue.photo.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.photo.domain.TraveloguePhoto; - -public interface TraveloguePhotoRepository extends JpaRepository { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java deleted file mode 100644 index 349c784d..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/place/dto/TraveloguePlaceResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package woowacourse.touroot.travelogue.place.dto; - -import java.util.List; - -public record TraveloguePlaceResponse( - String name, - List photoUrls, - String description, - String latitude, - String longitude -) { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java deleted file mode 100644 index 3395e41b..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/place/repsitory/TraveloguePlaceRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.touroot.travelogue.place.repsitory; - -import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; - -public interface TraveloguePlaceRepository extends JpaRepository { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java new file mode 100644 index 00000000..d2418d36 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java @@ -0,0 +1,76 @@ +package woowacourse.touroot.travelogue.service; + +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.day.dto.TravelogueDayResponse; +import woowacourse.touroot.travelogue.domain.day.service.TravelogueDayService; +import woowacourse.touroot.travelogue.domain.photo.service.TraveloguePhotoService; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.domain.place.dto.TraveloguePlaceResponse; +import woowacourse.touroot.travelogue.domain.place.service.TraveloguePlaceService; +import woowacourse.touroot.travelogue.dto.TravelogueResponse; + +@RequiredArgsConstructor +@Service +public class TravelogueFacadeService { + + private final TravelogueService travelogueService; + private final TravelogueDayService travelogueDayService; + private final TraveloguePlaceService traveloguePlaceService; + private final TraveloguePhotoService traveloguePhotoService; + + public TravelogueResponse findTravelogueById(Long id) { + Travelogue travelogue = travelogueService.getTravelogueById(id); + + return TravelogueResponse.builder() + .id(travelogue.getId()) + .title(travelogue.getTitle()) + .thumbnail(travelogue.getThumbnail()) + .days(findDaysOfTravelogue(travelogue)) + .build(); + } + + private List findDaysOfTravelogue(Travelogue travelogue) { + List travelogueDays = travelogueDayService.findDaysByTravelogue(travelogue); + + return travelogueDays.stream() + .sorted(Comparator.comparing(TravelogueDay::getOrder)) + .map(this::getTravelogueDayResponse) + .toList(); + } + + private TravelogueDayResponse getTravelogueDayResponse(TravelogueDay day) { + return TravelogueDayResponse.builder() + .id(day.getId()) + .places(findPlacesOfTravelogueDay(day)) + .build(); + } + + private List findPlacesOfTravelogueDay(TravelogueDay travelogueDay) { + List places = traveloguePlaceService.findTraveloguePlaceByDay(travelogueDay); + + return places.stream() + .sorted(Comparator.comparing(TraveloguePlace::getOrder)) + .map(this::getTraveloguePlaceResponse) + .toList(); + } + + private TraveloguePlaceResponse getTraveloguePlaceResponse(TraveloguePlace place) { + return TraveloguePlaceResponse.builder() + .id(place.getId()) + .name(place.getName()) + .description(place.getDescription()) + .lat(place.getLatitude()) + .lng(place.getLongitude()) + .photoUrls(findPhotoUrlsOfTraveloguePlace(place)) + .build(); + } + + private List findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) { + return traveloguePhotoService.findPhotoUrlsByPlace(place); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java index fc1a7ed8..41fe3571 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java @@ -1,17 +1,10 @@ package woowacourse.touroot.travelogue.service; -import java.util.Comparator; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import woowacourse.touroot.travelogue.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.day.dto.TravelogueDayResponse; +import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.dto.TravelogueResponse; -import woowacourse.touroot.travelogue.photo.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.place.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.place.dto.TraveloguePlaceResponse; import woowacourse.touroot.travelogue.repository.TravelogueRepository; @RequiredArgsConstructor @@ -21,39 +14,8 @@ public class TravelogueService { private final TravelogueRepository travelogueRepository; @Transactional(readOnly = true) - public TravelogueResponse findTravelogueById(Long id) { - Travelogue travelogue = travelogueRepository.findById(id).get(); - - return new TravelogueResponse(travelogue.getTitle(), travelogue.getThumbnail(), getDayResponses(travelogue)); - } - - private List getDayResponses(Travelogue travelogue) { - return travelogue.getTravelogueDays() - .stream() - .sorted(Comparator.comparing(TravelogueDay::getOrder)) - .map(travelogueDay -> new TravelogueDayResponse(getPlaceResponses(travelogueDay))) - .toList(); - } - - private List getPlaceResponses(TravelogueDay day) { - return day.getTraveloguePlaces() - .stream() - .sorted(Comparator.comparing(TraveloguePlace::getOrder)) - .map(traveloguePlace -> new TraveloguePlaceResponse( - traveloguePlace.getPlace().getName(), - getPhotoUrls(traveloguePlace), - traveloguePlace.getDescription(), - traveloguePlace.getPlace().getLatitude(), - traveloguePlace.getPlace().getLongitude() - )) - .toList(); - } - - private List getPhotoUrls(TraveloguePlace traveloguePlace) { - return traveloguePlace.getTraveloguePhotos() - .stream() - .sorted(Comparator.comparing(TraveloguePhoto::getOrder)) - .map(TraveloguePhoto::getKey) - .toList(); + public Travelogue getTravelogueById(Long id) { + return travelogueRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기입니다.")); } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index bedfde4e..ea33582a 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -39,3 +39,27 @@ spring: dialect: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: none +--- +server: + port: 8081 +spring: + config: + activate: + on-profile: test + datasource: + url: jdbc:h2:mem:test + h2: + console: + enabled: true + path: /h2-console + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create + defer-datasource-initialization: true + sql: + init: + mode: never diff --git a/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java b/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java new file mode 100644 index 00000000..2cd03d48 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java @@ -0,0 +1,13 @@ +package woowacourse.touroot.global; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +@Retention(RetentionPolicy.RUNTIME) +public @interface AcceptanceTest { +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java new file mode 100644 index 00000000..adfa3748 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java @@ -0,0 +1,46 @@ +package woowacourse.touroot.travelogue.controller; + +import static org.hamcrest.Matchers.is; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +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.web.server.LocalServerPort; +import woowacourse.touroot.global.AcceptanceTest; +import woowacourse.touroot.utils.DatabaseCleaner; +import woowacourse.touroot.utils.TestFixture; + +@AcceptanceTest +class TravelogueControllerTest { + + @LocalServerPort + private int port; + + @Autowired + private DatabaseCleaner databaseCleaner; + + @Autowired + private TestFixture testFixture; + + @BeforeEach + void setUp() { + RestAssured.port = port; + + databaseCleaner.executeTruncate(); + testFixture.initTravelogueTestData(); + } + + @DisplayName("여행기를 상세 조회한다.") + @Test + void findTravelogue() { + RestAssured.given().log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues/1") + .then().log().all() + .statusCode(200) + .body("title", is("여행기 1")); + } +} \ No newline at end of file diff --git a/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java b/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java new file mode 100644 index 00000000..264e6f63 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java @@ -0,0 +1,74 @@ +package woowacourse.touroot.utils; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.metamodel.EntityType; +import jakarta.transaction.Transactional; +import java.util.List; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("test") +public class DatabaseCleaner { + + public static final String CAMEL_CASE = "([a-z])([A-Z])"; + public static final String SNAKE_CASE = "$1_$2"; + private static final String TRUNCATE_TABLE = "TRUNCATE TABLE %s"; + private static final String ALTER_COLUMN_ID = "ALTER TABLE %s ALTER COLUMN id RESTART WITH 1"; + public static final String INTEGRITY_FALSE = "SET REFERENTIAL_INTEGRITY FALSE"; + public static final String INTEGRITY_TRUE = "SET REFERENTIAL_INTEGRITY TRUE"; + + @PersistenceContext + private EntityManager entityManager; + + private List tableNames; + + @PostConstruct + public void findTableNames() { + tableNames = entityManager.getMetamodel().getEntities().stream() + .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null) + .map(DatabaseCleaner::convertCamelToSnake) + .toList(); + } + + private static String convertCamelToSnake(final EntityType e) { + return e.getName() + .replaceAll(CAMEL_CASE, SNAKE_CASE) + .toLowerCase(); + } + + @Transactional + public void executeTruncate() { + entityManager.clear(); + + disableIntegrity(); + for (String tableName : tableNames) { + truncateTable(tableName); + resetIdColumn(tableName); + } + enableIntegrity(); + } + + private void disableIntegrity() { + entityManager.createNativeQuery(INTEGRITY_FALSE) + .executeUpdate(); + } + + private void truncateTable(final String tableName) { + entityManager.createNativeQuery(String.format(TRUNCATE_TABLE, tableName)) + .executeUpdate(); + } + + private void resetIdColumn(final String tableName) { + entityManager.createNativeQuery(String.format(ALTER_COLUMN_ID, tableName)) + .executeUpdate(); + } + + private void enableIntegrity() { + entityManager.createNativeQuery(INTEGRITY_TRUE) + .executeUpdate(); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java new file mode 100644 index 00000000..32a27c68 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java @@ -0,0 +1,70 @@ +package woowacourse.touroot.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.place.repository.PlaceRepository; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.day.repository.TravelogueDayRepository; +import woowacourse.touroot.travelogue.domain.photo.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.photo.repository.TraveloguePhotoRepository; +import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.domain.place.repsitory.TraveloguePlaceRepository; +import woowacourse.touroot.travelogue.repository.TravelogueRepository; + +@Component +@Profile("test") +public class TestFixture { + + @Autowired + TravelogueRepository travelogueRepository; + + @Autowired + TravelogueDayRepository travelogueDayRepository; + + @Autowired + TraveloguePlaceRepository traveloguePlaceRepository; + + @Autowired + TraveloguePhotoRepository traveloguePhotoRepository; + + @Autowired + private PlaceRepository placeRepository; + + public static Travelogue getTravelogue(String name, String thumbnail) { + return new Travelogue(name, thumbnail); + } + + public static TravelogueDay getTravelogueDay(Integer order, Travelogue travelogue) { + return new TravelogueDay(order, travelogue); + } + + public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { + return new Place(name, latitude, longitude, googlePlaceId); + } + + public static TraveloguePlace getTraveloguePlace(Integer order, String description, Place place, + TravelogueDay travelogueDay) { + return new TraveloguePlace(order, description, place, travelogueDay); + } + + public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { + return new TraveloguePhoto(key, order, traveloguePlace); + } + + public void initTravelogueTestData() { + Travelogue travelogue = getTravelogue("여행기 1", "썸네일.png"); + TravelogueDay travelogueDay = getTravelogueDay(1, travelogue); + Place place = getPlace("장소 1", "33.3333", "127.2727", ""); + TraveloguePlace traveloguePlace = getTraveloguePlace(1, "좋은 장소", place, travelogueDay); + TraveloguePhoto traveloguePhoto = getTraveloguePhoto("image", 1, traveloguePlace); + + travelogueRepository.save(travelogue); + travelogueDayRepository.save(travelogueDay); + placeRepository.save(place); + traveloguePlaceRepository.save(traveloguePlace); + traveloguePhotoRepository.save(traveloguePhoto); + } +} From 4626457eab1a5bd3cdfadd4858b4880b7b57493b Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:51:32 +0900 Subject: [PATCH 024/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: TravelPlanController Swagger label 여행기 -> 여행 계획으로 변경 * fix: PlanPlaceCreateRequest 장소명과 설명 예시 바뀐 곳 수정 * feat: 여행 계획 상세 조회 API 구현 * feat: 여행 계획 상세 조회 swagger 설명 추가 * feat: TravelPlanController Swagger에 Error 응답 추가 * feat: GlobalExceptionHandler 추가 * feat: TravelPlan 응답에서 날짜, 장소 순서별로 정렬 추가 * fix: TravelPlanResponse에 TravelPlan id 추가 * test: TravelPlanController 테스트 추가 * test: TravelPlanService 테스트 추가 --- .../exception/GlobalExceptionHandler.java | 20 +++ .../exception/dto/ExceptionResponse.java | 4 + .../controller/TravelPlanController.java | 53 +++++-- .../travelplan/domain/TravelPlanDay.java | 6 + .../{ => request}/PlanDayCreateRequest.java | 2 +- .../PlanLocationCreateRequest.java | 2 +- .../{ => request}/PlanPlaceCreateRequest.java | 8 +- .../TravelPlanCreateRequest.java | 4 +- .../TravelPlanCreateResponse.java | 2 +- .../dto/response/TravelPlanDayResponse.java | 25 ++++ .../response/TravelPlanLocationResponse.java | 19 +++ .../dto/response/TravelPlanPlaceResponse.java | 25 ++++ .../dto/response/TravelPlanResponse.java | 26 ++++ .../travelplan/service/TravelPlanService.java | 41 +++++- .../controller/TravelPlanControllerTest.java | 134 ++++++++++++++++++ .../service/TravelPlanServiceTest.java | 121 ++++++++++++++++ .../touroot/utils/TestFixture.java | 40 ++++++ 17 files changed, 510 insertions(+), 22 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java rename backend/src/main/java/woowacourse/touroot/travelplan/dto/{ => request}/PlanDayCreateRequest.java (94%) rename backend/src/main/java/woowacourse/touroot/travelplan/dto/{ => request}/PlanLocationCreateRequest.java (90%) rename backend/src/main/java/woowacourse/touroot/travelplan/dto/{ => request}/PlanPlaceCreateRequest.java (93%) rename backend/src/main/java/woowacourse/touroot/travelplan/dto/{ => request}/TravelPlanCreateRequest.java (92%) rename backend/src/main/java/woowacourse/touroot/travelplan/dto/{ => response}/TravelPlanCreateResponse.java (76%) create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..e8612f41 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,20 @@ +package woowacourse.touroot.global.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException exception) { + log.info("BAD_REQUEST_EXCEPTION :: message = {}", exception.getMessage()); + ExceptionResponse data = new ExceptionResponse(exception.getMessage()); + return ResponseEntity.badRequest() + .body(data); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java b/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java new file mode 100644 index 00000000..8418e1c5 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java @@ -0,0 +1,4 @@ +package woowacourse.touroot.global.exception.dto; + +public record ExceptionResponse(String message) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java index de03821f..9b5ad32a 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java @@ -1,19 +1,22 @@ package woowacourse.touroot.travelplan.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse; +import org.springframework.web.bind.annotation.*; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; import woowacourse.touroot.travelplan.service.TravelPlanService; -@Tag(name = "여행기") +@Tag(name = "여행 계획") @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/travel-plans") @@ -21,11 +24,39 @@ public class TravelPlanController { private final TravelPlanService travelPlanService; - @Operation(summary = "여행기 생성") + @Operation( + summary = "여행 계획 생성", + responses = { + @ApiResponse( + responseCode = "400", + description = "Body에 유효하지 않은 값이 존재하거나 지난 날짜에 대한 계획을 생성할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + } + ) @PostMapping - public ResponseEntity createTravelPlan(@Valid @RequestBody TravelPlanCreateRequest request) { + public ResponseEntity createTravelPlan( + @Valid @RequestBody TravelPlanCreateRequest request + ) { TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request); - return ResponseEntity.ok() - .body(data); + return ResponseEntity.ok(data); + } + + @Operation( + summary = "여행 계획 상세 조회", + responses = { + @ApiResponse( + responseCode = "400", + description = "존재하지 않은 여행 계획을 조회할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + } + ) + @GetMapping("/{id}") + public ResponseEntity readTravelPlan( + @Parameter(description = "여행 계획 id") @PathVariable Long id + ) { + TravelPlanResponse data = travelPlanService.readTravelPlan(id); + return ResponseEntity.ok(data); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java index fe08d0d3..a5c909b0 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; +import java.time.LocalDate; import java.util.List; @Getter @@ -32,4 +33,9 @@ public class TravelPlanDay extends BaseEntity { public TravelPlanDay(int order, TravelPlan plan) { this(null, order, plan, null); } + + public LocalDate getCurrentDate() { + LocalDate startDate = plan.getStartDate(); + return startDate.plusDays(order); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java similarity index 94% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java index 99aedbca..12aa0ed3 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanDayCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java similarity index 90% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java index 5ba1bd42..ddac4d0f 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanLocationCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java similarity index 93% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java index 182cf88b..08cc8dd6 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java @@ -1,17 +1,19 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.Builder; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.travelplan.domain.TravelPlanDay; import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +@Builder public record PlanPlaceCreateRequest( - @Schema(description = "여행 장소 이름", example = "신나는 여행 장소") + @Schema(description = "여행 장소 이름", example = "잠실한강공원") @NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName, - @Schema(description = "여행 장소 설명", example = "잠실한강공원") + @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") String description, @Schema(description = "여행 장소 순서", example = "1") @NotNull diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java similarity index 92% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java index e88e2fce..ab988ebb 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java @@ -1,13 +1,15 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.Builder; import woowacourse.touroot.travelplan.domain.TravelPlan; import java.time.LocalDate; import java.util.List; +@Builder public record TravelPlanCreateRequest( @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") @NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.") diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java index 26b147be..66c1cd08 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/TravelPlanCreateResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto; +package woowacourse.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java new file mode 100644 index 00000000..62493e38 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; + +import java.time.LocalDate; +import java.util.List; + +@Builder +public record TravelPlanDayResponse( + @Schema(description = "여행 일자") LocalDate date, + @Schema(description = "여행 장소별 정보") List places +) { + + public static TravelPlanDayResponse of( + TravelPlanDay planDay, + List places + ) { + return TravelPlanDayResponse.builder() + .date(planDay.getCurrentDate()) + .places(places) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java new file mode 100644 index 00000000..0824ecb8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java @@ -0,0 +1,19 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.place.domain.Place; + +@Builder +public record TravelPlanLocationResponse( + @Schema(description = "여행 장소 위도") String lat, + @Schema(description = "여행 계획 경도") String lng +) { + + public static TravelPlanLocationResponse from(Place place) { + return TravelPlanLocationResponse.builder() + .lat(place.getLatitude()) + .lng(place.getLongitude()) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java new file mode 100644 index 00000000..118e67cb --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; + +@Builder +public record TravelPlanPlaceResponse( + @Schema(description = "여행 장소 이름") String placeName, + @Schema(description = "여행 장소 위치") TravelPlanLocationResponse location, + @Schema(description = "여행 장소 설명") String description +) { + + public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) { + Place place = planPlace.getPlace(); + TravelPlanLocationResponse locationResponse = TravelPlanLocationResponse.from(place); + + return TravelPlanPlaceResponse.builder() + .placeName(place.getName()) + .location(locationResponse) + .description(planPlace.getDescription()) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java new file mode 100644 index 00000000..ecc6d580 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java @@ -0,0 +1,26 @@ +package woowacourse.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.travelplan.domain.TravelPlan; + +import java.time.LocalDate; +import java.util.List; + +@Builder +public record TravelPlanResponse( + @Schema(description = "여행 계획 id") Long id, + @Schema(description = "여행 계획 제목") String title, + @Schema(description = "여행 시작일") LocalDate startDate, + @Schema(description = "여행 계획 날짜별 정보") List days +) { + + public static TravelPlanResponse of(TravelPlan travelPlan, List days) { + return TravelPlanResponse.builder() + .id(travelPlan.getId()) + .title(travelPlan.getTitle()) + .startDate(travelPlan.getStartDate()) + .days(days) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java index f962ea03..e4ef1623 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java @@ -3,18 +3,24 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; import woowacourse.touroot.travelplan.domain.TravelPlan; import woowacourse.touroot.travelplan.domain.TravelPlanDay; -import woowacourse.touroot.travelplan.dto.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.PlanPlaceCreateRequest; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanDayResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanPlaceResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; import woowacourse.touroot.travelplan.repository.TravelPlanRepository; +import java.util.Comparator; import java.util.List; @RequiredArgsConstructor @@ -39,6 +45,7 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) { for (PlanDayCreateRequest dayRequest : request.days()) { + // TODO: order는 배열 index로 변경 TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan)); createPlanPlace(dayRequest.places(), travelPlanDay); } @@ -46,6 +53,7 @@ private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTrav private void createPlanPlace(List request, TravelPlanDay travelPlanDay) { for (PlanPlaceCreateRequest planRequest : request) { + // TODO: order는 배열 index로 변경 Place place = getPlace(planRequest); travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place)); } @@ -58,4 +66,29 @@ private Place getPlace(PlanPlaceCreateRequest planRequest) { planRequest.location().lng() ).orElseGet(() -> placeRepository.save(planRequest.toPlace())); } + + @Transactional(readOnly = true) + public TravelPlanResponse readTravelPlan(Long planId) { + TravelPlan travelPlan = getTravelPlanById(planId); + return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + } + + private TravelPlan getTravelPlanById(Long planId) { + return travelPlanRepository.findById(planId) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); + } + + private List getTravelPlanDayResponses(TravelPlan travelPlan) { + return travelPlan.getDays().stream() + .sorted(Comparator.comparing(TravelPlanDay::getOrder)) + .map(day -> TravelPlanDayResponse.of(day, getTravelPlanPlaceResponses(day))) + .toList(); + } + + private List getTravelPlanPlaceResponses(TravelPlanDay day) { + return day.getPlaces().stream() + .sorted(Comparator.comparing(TravelPlanPlace::getOrder)) + .map(TravelPlanPlaceResponse::from) + .toList(); + } } diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java new file mode 100644 index 00000000..9504e570 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -0,0 +1,134 @@ +package woowacourse.touroot.travelplan.controller; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +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.web.server.LocalServerPort; +import woowacourse.touroot.global.AcceptanceTest; +import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.utils.DatabaseCleaner; +import woowacourse.touroot.utils.TestFixture; + +import java.time.LocalDate; +import java.util.List; + +import static org.hamcrest.Matchers.is; + +@DisplayName("여행 계획 컨트롤러") +@AcceptanceTest +class TravelPlanControllerTest { + + @LocalServerPort + private int port; + private final DatabaseCleaner databaseCleaner; + private final TestFixture testFixture; + + @Autowired + public TravelPlanControllerTest(DatabaseCleaner databaseCleaner, TestFixture testFixture) { + this.databaseCleaner = databaseCleaner; + this.testFixture = testFixture; + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + databaseCleaner.executeTruncate(); + } + + @DisplayName("여행 계획 컨트롤러는 생성 요청이 들어올 때 200을 응답한다.") + @Test + void createTravelPlan() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .post("/api/v1/travel-plans") + .then().log().all() + .statusCode(200) + .body("id", is(1)); + } + + @DisplayName("여행 계획 컨트롤러는 지난 날짜로 생성 요청이 들어올 때 400을 응답한다.") + @Test + void createTravelPlanWithInvalidStartDate() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MIN) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .post("/api/v1/travel-plans") + .then().log().all() + .statusCode(400) + .body("message", is("지난 날짜에 대한 계획은 작성할 수 없습니다.")); + } + + @DisplayName("여행 계획 컨트롤러는 상세 조회 요청이 들어오면 200을 응답한다.") + @Test + void readTravelPlan() { + // given + testFixture.initTravelPlanTestData(); + long id = 1L; + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().log().all() + .get("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(200) + .body("id", is(1)); + } + + @DisplayName("여행 계획 컨트롤러는 존재하지 않는 상세 조회 요청이 들어오면 400을 응답한다.") + @Test + void readTravelPlanWithNonExist() { + // given + long id = 1L; + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().log().all() + .get("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행 계획입니다.")); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java new file mode 100644 index 00000000..25bba77e --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java @@ -0,0 +1,121 @@ +package woowacourse.touroot.travelplan.service; + +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 woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; +import woowacourse.touroot.utils.DatabaseCleaner; +import woowacourse.touroot.utils.TestFixture; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("여행 계획 서비스") +@ActiveProfiles("test") +// TODO: 양방향 해결 후 @DataJpaTest로 변경 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +class TravelPlanServiceTest { + + private final TravelPlanService travelPlanService; + private final DatabaseCleaner databaseCleaner; + private final TestFixture testFixture; + + @Autowired + public TravelPlanServiceTest( + TravelPlanService travelPlanService, + DatabaseCleaner databaseCleaner, + TestFixture testFixture + ) { + this.travelPlanService = travelPlanService; + this.databaseCleaner = databaseCleaner; + this.testFixture = testFixture; + } + + @DisplayName("여행 계획 서비스는 여행 계획 생성 시 생성된 id를 응답한다.") + @Test + void createTravelPlan() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayCreateRequest)) + .build(); + + // when + TravelPlanCreateResponse actual = travelPlanService.createTravelPlan(request); + + // then + assertThat(actual.id()).isEqualTo(1L); + } + + @DisplayName("여행 계획 서비스는 지난 날짜로 여행 계획 생성 시 예외를 반환한다.") + @Test + void createTravelPlanWithInvalidStartDate() { + // given + PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .order(0) + .location(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MIN) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then= + assertThatThrownBy(() -> travelPlanService.createTravelPlan(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); + } + + @DisplayName("여행 계획 서비스는 여행 계획 조회 시 상세 정보를 반환한다.") + @Test + void readTravelPlan() { + // given + databaseCleaner.executeTruncate(); + testFixture.initTravelPlanTestData(); + Long id = 1L; + + // when + TravelPlanResponse actual = travelPlanService.readTravelPlan(id); + + // then + assertThat(actual.id()).isEqualTo(id); + } + + @DisplayName("여행 계획 서비스는 여행 계획 조회 시 상세 정보를 반환한다.") + @Test + void readTravelPlanWitNonExist() { + // given + databaseCleaner.executeTruncate(); + Long id = 1L; + + // when & then + assertThatThrownBy(() -> travelPlanService.readTravelPlan(id)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행 계획입니다."); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java index 32a27c68..98625352 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java @@ -13,6 +13,14 @@ import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; import woowacourse.touroot.travelogue.domain.place.repsitory.TraveloguePlaceRepository; import woowacourse.touroot.travelogue.repository.TravelogueRepository; +import woowacourse.touroot.travelplan.domain.TravelPlan; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; +import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; +import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; +import woowacourse.touroot.travelplan.repository.TravelPlanRepository; + +import java.time.LocalDate; @Component @Profile("test") @@ -33,6 +41,14 @@ public class TestFixture { @Autowired private PlaceRepository placeRepository; + @Autowired + private TravelPlanRepository travelPlanRepository; + @Autowired + private TravelPlanDayRepository travelPlanDayRepository; + @Autowired + private TravelPlanPlaceRepository travelPlanPlaceRepository; + + public static Travelogue getTravelogue(String name, String thumbnail) { return new Travelogue(name, thumbnail); } @@ -54,6 +70,18 @@ public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, Trav return new TraveloguePhoto(key, order, traveloguePlace); } + public static TravelPlan getTravelPlan(String title, LocalDate startDate) { + return new TravelPlan(title, startDate); + } + + public static TravelPlanDay getTravelPlanDay(int order, TravelPlan travelPlan) { + return new TravelPlanDay(order, travelPlan); + } + + public static TravelPlanPlace getTravelPlanPlace(String description, int order, Place place, TravelPlanDay day) { + return new TravelPlanPlace(description, order, day, place); + } + public void initTravelogueTestData() { Travelogue travelogue = getTravelogue("여행기 1", "썸네일.png"); TravelogueDay travelogueDay = getTravelogueDay(1, travelogue); @@ -67,4 +95,16 @@ public void initTravelogueTestData() { traveloguePlaceRepository.save(traveloguePlace); traveloguePhotoRepository.save(traveloguePhoto); } + + public void initTravelPlanTestData() { + TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX); + TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); + Place place = getPlace("장소", "37.5175896", "127.0867236", ""); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace("설명", 0, place, travelPlanDay); + + travelPlanRepository.save(travelPlan); + travelPlanDayRepository.save(travelPlanDay); + placeRepository.save(place); + travelPlanPlaceRepository.save(travelPlanPlace); + } } From 2fc98aa1b6cc238782b177eac554ad63490dd7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:42:01 +0900 Subject: [PATCH 025/108] =?UTF-8?q?[Feature]=20-=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: OAuth 요청에 대한 응답 DTO 작성 * chore: Jasypt 의존성 추가 * feat: 카카오 OAuth Client 객체 구현 * feat: OAuthProvider 구현 * feat: Login 서비스, 컨트롤러 구현 * feat: 소셜 로그인 회원가입 분기 흐름 구현 * feat: jwt를 통한 로그인 기능 구현 * chore: 디버깅 출력 문구 삭제 * fix: jasypt 시크릿 키를 github action에서 환경 변수로 지정 * fix: github action에서 빌드 시 jasypt secret key를 환경변수로 받도록 변경 - `build.gradle`에 테스트 시 jasypt secret key를 환경변수로 지정한다. - `be-cd-dev.yml`와 `be-ci.yml`에서 -P 옵션으로 jasypt secret key를 환경변수로 지정한다. * feat: 카카오 OAuth 로그인 redirect uri 프로파일별로 분리 * fix: 테스트 용 config yml 파일 분리 및 테스트에서 jasypt 제거 * chore: 로컬용 jwt 비밀키와 개발 서버용 키 분리 * chore: 데이터베이스 정보 관리 환경변수 방식에서 jasypt 방식으로 변경 * refactor: DTO inner 클래스 가독성 위해서 별도의 record로 분리 * refactor: 카카오 유저 정보 응답 DTO nested record로 개선 * style: 괄호 재배치, 공백 문자 가독성 개선 * refactor: RestClient 설정 기능 생성자에서 분리 개선 * refactor: 하드 코딩된 헤더 정보 미리 제공되는 상수로 변경 * refactor: 사용되지 않는 생성자 접근 제어 레벨 개선 * chore: github action에서 빌드 시 환경 변수를 지정하지 않도록 변경 --------- Co-authored-by: libienz Co-authored-by: nhlee98 Co-authored-by: 이낙헌 <95845037+nak-honest@users.noreply.github.com> --- backend/build.gradle | 4 + .../controller/LoginController.java | 24 ++++++ .../dto/KakaoAccessTokenResponse.java | 19 +++++ .../authentication/dto/LoginResponse.java | 4 + .../dto/OauthUserInformationResponse.java | 30 ++++++++ .../infrastructure/JwtTokenProvider.java | 37 +++++++++ .../infrastructure/KakaoOauthClient.java | 77 +++++++++++++++++++ .../infrastructure/KakaoOauthProvider.java | 16 ++++ .../authentication/service/LoginService.java | 33 ++++++++ .../touroot/global/config/JasyptConfig.java | 9 +++ .../touroot/member/domain/Member.java | 36 +++++++++ .../member/repository/MemberRepository.java | 10 +++ backend/src/main/resources/application.yml | 56 +++++++------- .../touroot/TourootApplicationTests.java | 13 ---- .../touroot/global/AcceptanceTest.java | 4 +- .../touroot/utils/DatabaseCleaner.java | 2 - .../touroot/utils/TestFixture.java | 4 +- .../src/test/resources/application-test.yml | 31 ++++++++ 18 files changed, 362 insertions(+), 47 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java create mode 100644 backend/src/main/java/woowacourse/touroot/member/domain/Member.java create mode 100644 backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java delete mode 100644 backend/src/test/java/woowacourse/touroot/TourootApplicationTests.java create mode 100644 backend/src/test/resources/application-test.yml diff --git a/backend/build.gradle b/backend/build.gradle index c631365e..83b5f7f5 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -28,6 +28,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java new file mode 100644 index 00000000..40fc3fae --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java @@ -0,0 +1,24 @@ +package woowacourse.touroot.authentication.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import woowacourse.touroot.authentication.dto.LoginResponse; +import woowacourse.touroot.authentication.service.LoginService; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/login") +public class LoginController { + + private final LoginService loginService; + + @GetMapping("/oauth/kakao") + public ResponseEntity login(@RequestParam(name = "code") String authorizationCode) { + return ResponseEntity.ok() + .body(loginService.login(authorizationCode)); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java new file mode 100644 index 00000000..47f5ec11 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java @@ -0,0 +1,19 @@ +package woowacourse.touroot.authentication.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoAccessTokenResponse( + @JsonProperty("token_type") + String tokenType, + @JsonProperty("access_token") + String accessToken, + @JsonProperty("expires_in") + Integer expiresIn, + @JsonProperty("refresh_token") + String refreshToken, + @JsonProperty("refresh_token_expires_in") + Integer refreshTokenExpiresIn, + @JsonProperty("scope") + String scope +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java new file mode 100644 index 00000000..156f99fc --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java @@ -0,0 +1,4 @@ +package woowacourse.touroot.authentication.dto; + +public record LoginResponse(String accessToken) { +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java new file mode 100644 index 00000000..3a453147 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java @@ -0,0 +1,30 @@ +package woowacourse.touroot.authentication.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record OauthUserInformationResponse( + @JsonProperty("id") + Long socialLoginId, + @JsonProperty("kakao_account") + KakaoAccount kakaoAccount +) { + + public String nickname() { + return kakaoAccount.kakaoProfile.nickname; + } + + public String profileImage() { + return kakaoAccount.kakaoProfile.image; + } + + private record KakaoAccount( + @JsonProperty("profile") KakaoProfile kakaoProfile + ) { + } + + private record KakaoProfile( + @JsonProperty("nickname") String nickname, + @JsonProperty("profile_image_url") String image + ) { + } +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java new file mode 100644 index 00000000..1b8f3ee9 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java @@ -0,0 +1,37 @@ +package woowacourse.touroot.authentication.infrastructure; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.util.Date; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import woowacourse.touroot.member.domain.Member; + +@Component +public class JwtTokenProvider { + + private static final String MEMBER_ID_KEY = "id"; + + private final String secretKey; + private final long validityInMilliseconds; + + public JwtTokenProvider( + @Value("${security.jwt.token.secret-key}") String secretKey, + @Value("${security.jwt.token.expire-length}") long validityInMilliseconds + ) { + this.secretKey = secretKey; + this.validityInMilliseconds = validityInMilliseconds; + } + + public String createToken(Member member) { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim(MEMBER_ID_KEY, member.getId()) + .setExpiration(validity) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java new file mode 100644 index 00000000..ccb2c1a3 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java @@ -0,0 +1,77 @@ +package woowacourse.touroot.authentication.infrastructure; + +import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.ClientHttpRequestFactories; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import woowacourse.touroot.authentication.dto.KakaoAccessTokenResponse; +import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; + +@Component +public class KakaoOauthClient { + + private final String userInformationRequestUri; + private final String accessTokenRequestUri; + private final String restApiKey; + private final String redirectUri; + private final RestClient restClient; + + public KakaoOauthClient( + @Value("${oauth.kakao.user-information-request-uri}") String userInformationRequestUri, + @Value("${oauth.kakao.access-token-request-uri}") String accessTokenRequestUri, + @Value("${oauth.kakao.rest-api-key}") String restApiKey, + @Value("${oauth.kakao.redirect-uri}") String redirectUri + ) { + this.userInformationRequestUri = userInformationRequestUri; + this.accessTokenRequestUri = accessTokenRequestUri; + this.restApiKey = restApiKey; + this.redirectUri = redirectUri; + this.restClient = buildRestClient(); + } + + private RestClient buildRestClient() { + ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS + .withConnectTimeout(Duration.ofSeconds(1)) + .withReadTimeout(Duration.ofSeconds(3)); + + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings); + + return RestClient.builder() + .requestFactory(requestFactory) + .build(); + } + + public OauthUserInformationResponse requestUserInformation(String authorizationCode) { + KakaoAccessTokenResponse kakaoAccessTokenResponse = requestAccessToken(authorizationCode); + + return restClient.get() + .uri(userInformationRequestUri) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + kakaoAccessTokenResponse.accessToken()) + .retrieve() + .toEntity(OauthUserInformationResponse.class) + .getBody(); + } + + private KakaoAccessTokenResponse requestAccessToken(String authorizationCode) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", authorizationCode); + params.add("client_id", restApiKey); + params.add("redirect_uri", redirectUri); + params.add("grant_type", "authorization_code"); + + return restClient.post() + .uri(accessTokenRequestUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(params) + .retrieve() + .toEntity(KakaoAccessTokenResponse.class) + .getBody(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java new file mode 100644 index 00000000..d23b7de7 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java @@ -0,0 +1,16 @@ +package woowacourse.touroot.authentication.infrastructure; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; + +@RequiredArgsConstructor +@Component +public class KakaoOauthProvider { + + private final KakaoOauthClient kakaoOauthClient; + + public OauthUserInformationResponse getUserInformation(String authorizationCode) { + return kakaoOauthClient.requestUserInformation(authorizationCode); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java b/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java new file mode 100644 index 00000000..9f02b4e7 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java @@ -0,0 +1,33 @@ +package woowacourse.touroot.authentication.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import woowacourse.touroot.authentication.dto.LoginResponse; +import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; +import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; +import woowacourse.touroot.authentication.infrastructure.KakaoOauthProvider; +import woowacourse.touroot.member.domain.Member; +import woowacourse.touroot.member.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final MemberRepository memberRepository; + private final KakaoOauthProvider oauthProvider; + private final JwtTokenProvider tokenProvider; + + public LoginResponse login(String code) { + OauthUserInformationResponse userInformation = oauthProvider.getUserInformation(code); + Member member = memberRepository.findByKakaoId(userInformation.socialLoginId()) + .orElseGet(() -> signUp(userInformation)); + + return new LoginResponse(tokenProvider.createToken(member)); + } + + private Member signUp(OauthUserInformationResponse userInformation) { + return memberRepository.save( + new Member(userInformation.socialLoginId(), userInformation.nickname(), userInformation.profileImage()) + ); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java new file mode 100644 index 00000000..2e9779c8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java @@ -0,0 +1,9 @@ +package woowacourse.touroot.global.config; + +import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableEncryptableProperties +public class JasyptConfig { +} diff --git a/backend/src/main/java/woowacourse/touroot/member/domain/Member.java b/backend/src/main/java/woowacourse/touroot/member/domain/Member.java new file mode 100644 index 00000000..898f5e87 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/member/domain/Member.java @@ -0,0 +1,36 @@ +package woowacourse.touroot.member.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import woowacourse.touroot.entity.BaseEntity; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Entity +public class Member extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long kakaoId; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private String profileImageUri; + + public Member(Long kakaoId, String nickname, String profileImageUri) { + this(null, kakaoId, nickname, profileImageUri); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java b/backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java new file mode 100644 index 00000000..fe748797 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package woowacourse.touroot.member.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.member.domain.Member; + +public interface MemberRepository extends JpaRepository { + + Optional findByKakaoId(Long kakaoId); +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index ea33582a..abccd82d 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,3 +1,18 @@ +oauth: + kakao: + user-information-request-uri: https://kapi.kakao.com/v2/user/me + access-token-request-uri: https://kauth.kakao.com/oauth/token + rest-api-key: ENC(i6J7NWUsDpXVbJrbSUcNFI3h0oc6v8PxuHShU9UA7EVuUNLtQN/ANII+8j5HjhGO) +jasypt: + encryptor: + algorithm: PBEWithMD5AndDES + iv-generator-classname: org.jasypt.iv.NoIvGenerator +--- +security: + jwt: + token: + secret-key: ENC(SNwFG2NQDZkmIK3nNoZFdwQ0ZxKuoe+qcw10ljdW941YEx/Qky9PEEl+wvAN9S1KR26D3a83SnU=) + expire-length: 1800000 spring: config: activate: @@ -18,7 +33,15 @@ spring: hibernate: ddl-auto: create-drop defer-datasource-initialization: true +oauth: + kakao: + redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao --- +security: + jwt: + token: + secret-key: ENC(L36WWjoZtP2nHHkqxDGlYLsMHMp+EBL2Fnl+X2de2KHk+PIfViyVM7rCYcbcFpo7yB4MaP++atU=) + expire-length: 1800000 spring: config: activate: @@ -28,9 +51,9 @@ spring: enabled: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ${SPRING_DATASOURCE_URL} - username: ${SPRING_DATASOURCE_USERNAME} - password: ${SPRING_DATASOURCE_PASSWORD} + url: ENC(FgbVXdH4a5/EkMxdmYfPhFKAOwn1w+/CnmWDcN9p6fOVP6mP9coMEYezCPCNf95h) + username: ENC(SJznQPqjlZuw3qf8kv9IJQ==) + password: ENC(HsOo6wWp//egPPsSG6Wf40eF1Q2sVKfGuH4zGTL81Mw=) jpa: show-sql: true properties: @@ -39,27 +62,6 @@ spring: dialect: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: none ---- -server: - port: 8081 -spring: - config: - activate: - on-profile: test - datasource: - url: jdbc:h2:mem:test - h2: - console: - enabled: true - path: /h2-console - jpa: - show-sql: true - properties: - hibernate: - format_sql: true - hibernate: - ddl-auto: create - defer-datasource-initialization: true - sql: - init: - mode: never +oauth: + kakao: + redirect-uri: http://api-dev.touroot.kr/api/v1/login/oauth/kakao diff --git a/backend/src/test/java/woowacourse/touroot/TourootApplicationTests.java b/backend/src/test/java/woowacourse/touroot/TourootApplicationTests.java deleted file mode 100644 index 8dde8ee9..00000000 --- a/backend/src/test/java/woowacourse/touroot/TourootApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package woowacourse.touroot; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class TourootApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java b/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java index 2cd03d48..d40fb01b 100644 --- a/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java +++ b/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java @@ -4,10 +4,10 @@ import java.lang.annotation.RetentionPolicy; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@ActiveProfiles("test") @Retention(RetentionPolicy.RUNTIME) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) public @interface AcceptanceTest { } diff --git a/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java b/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java index 264e6f63..ed2d2125 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java +++ b/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java @@ -7,11 +7,9 @@ import jakarta.persistence.metamodel.EntityType; import jakarta.transaction.Transactional; import java.util.List; -import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @Component -@Profile("test") public class DatabaseCleaner { public static final String CAMEL_CASE = "([a-z])([A-Z])"; diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java index 98625352..7f2aaac9 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java @@ -1,7 +1,6 @@ package woowacourse.touroot.utils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; @@ -23,7 +22,6 @@ import java.time.LocalDate; @Component -@Profile("test") public class TestFixture { @Autowired @@ -37,7 +35,7 @@ public class TestFixture { @Autowired TraveloguePhotoRepository traveloguePhotoRepository; - + @Autowired private PlaceRepository placeRepository; diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml new file mode 100644 index 00000000..858d7497 --- /dev/null +++ b/backend/src/test/resources/application-test.yml @@ -0,0 +1,31 @@ +oauth: + kakao: + user-information-request-uri: https://kapi.kakao.com/v2/user/me + access-token-request-uri: https://kauth.kakao.com/oauth/token + redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao + rest-api-key: test-api-key +security: + jwt: + token: + secret-key: test-TADG67STFSDAGSDFSG4567UTKJHFHSDFGSR231DF + expire-length: 1800000 +server: + port: 8081 +spring: + datasource: + url: jdbc:h2:mem:test + h2: + console: + enabled: true + path: /h2-console + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create + defer-datasource-initialization: true + sql: + init: + mode: never From 1c81f013301fb2fc5313f769e4467d30d680fd74 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:48:31 +0900 Subject: [PATCH 026/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: MethodArgumentNotValidException handler 추가 * fix: TravelPlan Request에서 order 제거 후 index 사용으로 변경 --- .../exception/GlobalExceptionHandler.java | 17 +++++++++++++++++ .../dto/request/PlanDayCreateRequest.java | 16 ++++++++-------- .../dto/request/PlanPlaceCreateRequest.java | 12 +++++------- .../dto/request/TravelPlanCreateRequest.java | 4 ++++ .../travelplan/service/TravelPlanService.java | 16 ++++++++-------- .../controller/TravelPlanControllerTest.java | 6 ++---- .../service/TravelPlanServiceTest.java | 6 ++---- 7 files changed, 46 insertions(+), 31 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java index e8612f41..c8f0872e 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import woowacourse.touroot.global.exception.dto.ExceptionResponse; @@ -13,8 +14,24 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(BadRequestException exception) { log.info("BAD_REQUEST_EXCEPTION :: message = {}", exception.getMessage()); + ExceptionResponse data = new ExceptionResponse(exception.getMessage()); return ResponseEntity.badRequest() .body(data); } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception + ) { + log.info("METHOD_ARGUMENT_NOT_VALID_EXCEPTION :: message = {}", exception.getMessage()); + + String message = exception.getBindingResult() + .getAllErrors() + .get(0) + .getDefaultMessage(); + ExceptionResponse data = new ExceptionResponse(message); + return ResponseEntity.badRequest() + .body(data); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java index 12aa0ed3..e58229f7 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java @@ -1,23 +1,23 @@ package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import woowacourse.touroot.travelplan.domain.TravelPlan; import woowacourse.touroot.travelplan.domain.TravelPlanDay; import java.util.List; public record PlanDayCreateRequest( - @Schema(description = "여행 계획 날짜", example = "1") - @NotNull(message = "날짜는 비어있을 수 없습니다.") - @Min(value = 0, message = "날짜는 1 이상이어야 합니다.") - int day, @Schema(description = "여행 장소 정보") - @NotNull(message = "여행 장소 정보는 비어있을 수 없습니다.") List places + @Valid + @Size(min = 1, message = "여행 장소는 한 개 이상이어야 합니다.") + @NotNull(message = "여행 장소 정보는 비어있을 수 없습니다.") + List places ) { - public TravelPlanDay toPlanDay(TravelPlan plan) { - return new TravelPlanDay(day, plan); + public TravelPlanDay toPlanDay(int order, TravelPlan plan) { + return new TravelPlanDay(order, plan); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java index 08cc8dd6..32f16489 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java @@ -1,7 +1,7 @@ package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Builder; @@ -15,14 +15,12 @@ public record PlanPlaceCreateRequest( @NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName, @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") String description, - @Schema(description = "여행 장소 순서", example = "1") - @NotNull - @Min(value = 0, message = "순서는 1 이상이어야 합니다.") - int order, - @NotNull PlanLocationCreateRequest location + @Valid + @NotNull(message = "위치는 비어있을 수 없습니다.") + PlanLocationCreateRequest location ) { - public TravelPlanPlace toPlanPlace(TravelPlanDay day, Place place) { + public TravelPlanPlace toPlanPlace(int order, TravelPlanDay day, Place place) { return new TravelPlanPlace(description, order, day, place); } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java index ab988ebb..bf986bc9 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java @@ -1,8 +1,10 @@ package woowacourse.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Builder; import woowacourse.touroot.travelplan.domain.TravelPlan; @@ -18,6 +20,8 @@ public record TravelPlanCreateRequest( @NotNull(message = "시작일은 비어있을 수 없습니다.") LocalDate startDate, @Schema(description = "여행 날짜 정보") + @Valid + @Size(min = 1, message = "여행 날짜는 하루 이상 있어야 합니다.") @NotNull(message = "여행 날짜 정보는 비어있을 수 없습니다.") List days ) { diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java index e4ef1623..714facfa 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java @@ -38,24 +38,24 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request travelPlan.validateStartDate(); TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); - createPlanDay(request, savedTravelPlan); + createPlanDay(request.days(), savedTravelPlan); return new TravelPlanCreateResponse(savedTravelPlan.getId()); } - private void createPlanDay(TravelPlanCreateRequest request, TravelPlan savedTravelPlan) { - for (PlanDayCreateRequest dayRequest : request.days()) { - // TODO: order는 배열 index로 변경 - TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(savedTravelPlan)); + private void createPlanDay(List request, TravelPlan savedTravelPlan) { + for (int order = 0; order < request.size(); order++) { + PlanDayCreateRequest dayRequest = request.get(order); + TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(order, savedTravelPlan)); createPlanPlace(dayRequest.places(), travelPlanDay); } } private void createPlanPlace(List request, TravelPlanDay travelPlanDay) { - for (PlanPlaceCreateRequest planRequest : request) { - // TODO: order는 배열 index로 변경 + for (int order = 0; order < request.size(); order++) { + PlanPlaceCreateRequest planRequest = request.get(order); Place place = getPlace(planRequest); - travelPlanPlaceRepository.save(planRequest.toPlanPlace(travelPlanDay, place)); + travelPlanPlaceRepository.save(planRequest.toPlanPlace(order, travelPlanDay, place)); } } diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java index 9504e570..f5a417e5 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -49,10 +49,9 @@ void createTravelPlan() { PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .order(0) .location(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MAX) @@ -78,10 +77,9 @@ void createTravelPlanWithInvalidStartDate() { PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .order(0) .location(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MIN) diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java index 25bba77e..ccbcb296 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java @@ -50,10 +50,9 @@ void createTravelPlan() { PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .order(0) .location(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MAX) @@ -75,10 +74,9 @@ void createTravelPlanWithInvalidStartDate() { PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .order(0) .location(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(0, List.of(planPlaceCreateRequest)); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MIN) From e128b398e6293ad8de1209a06de14b4c86e8fc82 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Mon, 22 Jul 2024 01:55:22 +0900 Subject: [PATCH 027/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=91=EC=84=B1=20API=20=EB=B0=8F=20=EC=97=AC?= =?UTF-8?q?=ED=96=89=EA=B8=B0=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 작성 API를 위한 DTO 구현 * feat: 여행기 사진 엔티티 생성자 매개변수 순서 변경 * feat: 여행기 작성 API 구현 * feat: 메인 페이지를 위한 여행기 전체 목록 조회 API 구현 * chore: EOL 추가 * refactor: 불필요한 Bean validation 제거 * refactor: embedded DTO bean validation 추가 * chore: 실수로 제거한 필드 추가 * refactor: 패키지 구조 변경 * chore: 불필요한 final 제거 * refactor: 여행기 관련 도메인 조회 기능아 정적 팩토리 메소드를 활용하도록 개선 --- .../controller/TravelogueController.java | 32 ++++++- .../{day/domain => }/TravelogueDay.java | 5 +- .../{photo/domain => }/TraveloguePhoto.java | 5 +- .../{place/domain => }/TraveloguePlace.java | 3 +- .../domain/day/dto/TravelogueDayResponse.java | 18 ---- .../day/service/TravelogueDayService.java | 28 ------ .../photo/service/TraveloguePhotoService.java | 25 ----- .../place/dto/TraveloguePlaceResponse.java | 28 ------ .../place/service/TraveloguePlaceService.java | 28 ------ .../dto/request/TravelogueDayRequest.java | 20 ++++ .../request/TravelogueLocationRequest.java | 14 +++ .../dto/request/TraveloguePhotoRequest.java | 17 ++++ .../dto/request/TraveloguePlaceRequest.java | 34 +++++++ .../dto/request/TravelogueRequest.java | 25 +++++ .../dto/response/TravelogueDayResponse.java | 22 +++++ .../response/TravelogueLocationResponse.java | 21 +++++ .../dto/response/TraveloguePlaceResponse.java | 29 ++++++ .../{ => response}/TravelogueResponse.java | 15 ++- .../repository/TravelogueDayRepository.java | 6 +- .../repository/TraveloguePhotoRepository.java | 6 +- .../TraveloguePlaceRepository.java | 8 +- .../service/TravelogueDayService.java | 48 ++++++++++ .../service/TravelogueFacadeService.java | 91 ++++++++++++------- .../service/TraveloguePhotoService.java | 42 +++++++++ .../service/TraveloguePlaceService.java | 61 +++++++++++++ .../travelogue/service/TravelogueService.java | 13 +++ .../controller/TravelogueControllerTest.java | 2 +- .../touroot/utils/TestFixture.java | 17 ++-- 28 files changed, 472 insertions(+), 191 deletions(-) rename backend/src/main/java/woowacourse/touroot/travelogue/domain/{day/domain => }/TravelogueDay.java (88%) rename backend/src/main/java/woowacourse/touroot/travelogue/domain/{photo/domain => }/TraveloguePhoto.java (84%) rename backend/src/main/java/woowacourse/touroot/travelogue/domain/{place/domain => }/TraveloguePlace.java (91%) delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java delete mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java rename backend/src/main/java/woowacourse/touroot/travelogue/dto/{ => response}/TravelogueResponse.java (64%) rename backend/src/main/java/woowacourse/touroot/travelogue/{domain/day => }/repository/TravelogueDayRepository.java (68%) rename backend/src/main/java/woowacourse/touroot/travelogue/{domain/photo => }/repository/TraveloguePhotoRepository.java (55%) rename backend/src/main/java/woowacourse/touroot/travelogue/{domain/place/repsitory => repository}/TraveloguePlaceRepository.java (54%) create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java create mode 100644 backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java index 41792d9d..57fd67c8 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java @@ -1,15 +1,25 @@ package woowacourse.touroot.travelogue.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.net.URI; import lombok.RequiredArgsConstructor; +import org.springdoc.core.converters.models.PageableAsQueryParam; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import woowacourse.touroot.travelogue.dto.TravelogueResponse; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; import woowacourse.touroot.travelogue.service.TravelogueFacadeService; @Tag(name = "여행기") @@ -20,9 +30,29 @@ public class TravelogueController { private final TravelogueFacadeService travelogueFacadeService; + @Operation(description = "여행기 작성") + @PostMapping + public ResponseEntity createTravelogue(@Valid @RequestBody TravelogueRequest request) { + TravelogueResponse response = travelogueFacadeService.createTravelogue(request); + + return ResponseEntity.created(URI.create("/api/v1/travelogues/" + response.id())) + .body(response); + } + @Operation(description = "여행기 상세 조회") @GetMapping("/{id}") public ResponseEntity findTravelogue(@Valid @PathVariable Long id) { return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id)); } + + @Operation(description = "여행기 메인 페이지 조회") + @PageableAsQueryParam + @GetMapping + public ResponseEntity> findMainPageTravelogues( + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Direction.DESC) + Pageable pageable + ) { + return ResponseEntity.ok(travelogueFacadeService.findTravelogues(pageable)); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/domain/TravelogueDay.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TravelogueDay.java similarity index 88% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/day/domain/TravelogueDay.java rename to backend/src/main/java/woowacourse/touroot/travelogue/domain/TravelogueDay.java index e3d306e9..9d15e83a 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/domain/TravelogueDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TravelogueDay.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.domain.day.domain; +package woowacourse.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -10,12 +10,13 @@ import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.travelogue.domain.Travelogue; @Getter +@EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/domain/TraveloguePhoto.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java similarity index 84% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/domain/TraveloguePhoto.java rename to backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java index bdc59bd6..38d64560 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/domain/TraveloguePhoto.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.domain.photo.domain; +package woowacourse.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -13,7 +13,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -35,7 +34,7 @@ public class TraveloguePhoto extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private TraveloguePlace traveloguePlace; - public TraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { + public TraveloguePhoto(Integer order, String key, TraveloguePlace traveloguePlace) { this(null, key, order, traveloguePlace); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/domain/TraveloguePlace.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java similarity index 91% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/place/domain/TraveloguePlace.java rename to backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java index 21942047..b09d2ac5 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/domain/TraveloguePlace.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.domain.place.domain; +package woowacourse.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -14,7 +14,6 @@ import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java deleted file mode 100644 index 15fbd212..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/dto/TravelogueDayResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package woowacourse.touroot.travelogue.domain.day.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import java.util.List; -import lombok.Builder; -import woowacourse.touroot.travelogue.domain.place.dto.TraveloguePlaceResponse; - -@Builder -public record TravelogueDayResponse( - @Schema(description = "여행기 일자 ID", example = "1") - @NotNull(message = "ID는 비어있을 수 없습니다.") - Long id, - @Schema(description = "여행기 장소 목록") - @NotNull(message = "여행기 장소 정보는 비어있을 수 없습니다.") - List places -) { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java deleted file mode 100644 index 93a99bbd..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/service/TravelogueDayService.java +++ /dev/null @@ -1,28 +0,0 @@ -package woowacourse.touroot.travelogue.domain.day.service; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.day.repository.TravelogueDayRepository; - -@RequiredArgsConstructor -@Service -public class TravelogueDayService { - - private final TravelogueDayRepository travelogueDayRepository; - - @Transactional(readOnly = true) - public List findDaysByTravelogue(Travelogue travelogue) { - return travelogueDayRepository.findByTravelogue(travelogue); - } - - @Transactional(readOnly = true) - public TravelogueDay findDayById(Long id) { - return travelogueDayRepository.findById(id) - .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 일자입니다.")); - } -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java deleted file mode 100644 index eb082eb8..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/service/TraveloguePhotoService.java +++ /dev/null @@ -1,25 +0,0 @@ -package woowacourse.touroot.travelogue.domain.photo.service; - -import java.util.Comparator; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import woowacourse.touroot.travelogue.domain.photo.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.photo.repository.TraveloguePhotoRepository; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; - -@RequiredArgsConstructor -@Service -public class TraveloguePhotoService { - - private final TraveloguePhotoRepository traveloguePhotoRepository; - - public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { - List photos = traveloguePhotoRepository.findByTraveloguePlace(traveloguePlace); - - return photos.stream() - .sorted(Comparator.comparing(TraveloguePhoto::getOrder)) - .map(TraveloguePhoto::getKey) - .toList(); - } -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java deleted file mode 100644 index d89220af..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/dto/TraveloguePlaceResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package woowacourse.touroot.travelogue.domain.place.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.util.List; -import lombok.Builder; - -@Builder -public record TraveloguePlaceResponse( - @Schema(description = "여행기 장소 ID", example = "1") - @NotNull(message = "ID는 비어있을 수 없습니다.") - Long id, - @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") - @NotBlank(message = "여행기 장소 이름은 비어있을 수 없습니다.") - String name, - @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") - @NotBlank(message = "여행기 장소 설명은 비어있을 수 없습니다.") - String description, - @Schema(description = "여행기 장소 위도", example = "37.5175896") - @NotBlank(message = "여행기 장소 위도는 비어있을 수 없습니다.") - String lat, - @Schema(description = "여행기 장소 설명", example = "127.0867236") - @NotBlank(message = "여행기 장소 경도는 비어있을 수 없습니다.") - String lng, - List photoUrls -) { -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java deleted file mode 100644 index 9a6694de..00000000 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/service/TraveloguePlaceService.java +++ /dev/null @@ -1,28 +0,0 @@ -package woowacourse.touroot.travelogue.domain.place.service; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.domain.place.repsitory.TraveloguePlaceRepository; - -@RequiredArgsConstructor -@Service -public class TraveloguePlaceService { - - private final TraveloguePlaceRepository traveloguePlaceRepository; - - @Transactional(readOnly = true) - public List findTraveloguePlaceByDay(TravelogueDay travelogueDay) { - return traveloguePlaceRepository.findByTravelogueDay(travelogueDay); - } - - @Transactional(readOnly = true) - public TraveloguePlace findTraveloguePlaceById(Long id) { - return traveloguePlaceRepository.findById(id) - .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 장소입니다.")); - } -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java new file mode 100644 index 00000000..9a84a0ed --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java @@ -0,0 +1,20 @@ +package woowacourse.touroot.travelogue.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; + +public record TravelogueDayRequest( + @Schema(description = "여행기 장소 목록") + @NotNull(message = "여행기 장소 목록은 비어있을 수 없습니다.") + @Valid + List places +) { + + public TravelogueDay toTravelogueDay(int order, Travelogue travelogue) { + return new TravelogueDay(order, travelogue); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java new file mode 100644 index 00000000..9f995d1f --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java @@ -0,0 +1,14 @@ +package woowacourse.touroot.travelogue.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record TravelogueLocationRequest( + @Schema(description = "여행기 장소 위도", example = "37.5175896") + @NotNull(message = "여행기 장소 위도는 비어있을 수 없습니다.") + String lat, + @Schema(description = "여행기 장소 경도", example = "127.0867236") + @NotNull(message = "여행기 장소 경도는 비어있을 수 없습니다.") + String lng +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java new file mode 100644 index 00000000..9e07e8a0 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java @@ -0,0 +1,17 @@ +package woowacourse.touroot.travelogue.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; + +public record TraveloguePhotoRequest( + @Schema(description = "여행기 장소 사진 Key", example = "photo.png") + @NotNull(message = "여행기 장소 사진 Key 값은 비어있을 수 없습니다.") + String key +) { + + public TraveloguePhoto toTraveloguePhoto(int order, TraveloguePlace traveloguePlace) { + return new TraveloguePhoto(order, key, traveloguePlace); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java new file mode 100644 index 00000000..fe6c92a1 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -0,0 +1,34 @@ +package woowacourse.touroot.travelogue.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; + +public record TraveloguePlaceRequest( + @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") + @NotNull(message = "여행기 장소 이름은 비어있을 수 없습니다.") + String name, + @Schema(description = "여행기 장소 위치 정보") + @NotNull(message = "여행기 장소 위치 정보는 비어있을 수 없습니다.") + @Valid + TravelogueLocationRequest location, + @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") + String description, + @Schema(description = "여행기 장소 사진") + @NotNull(message = "여행기 장소 사진은 비어있을 수 없습니다.") + @Valid + List photos +) { + + public TraveloguePlace toTraveloguePlace(int order, Place place, TravelogueDay travelogueDay) { + return new TraveloguePlace(order, description, place, travelogueDay); + } + + public Place toPlace() { + return new Place(name, location.lat(), location.lng()); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java new file mode 100644 index 00000000..1450b8c8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.travelogue.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import woowacourse.touroot.travelogue.domain.Travelogue; + +public record TravelogueRequest( + @Schema(description = "여행기 제목", example = "서울 강남 여행기") + @NotNull(message = "여행기 제목은 비어있을 수 없습니다.") + String title, + @Schema(description = "여행기 섬네일", example = "https://thumbnail.png") + @NotNull(message = "여행기 섬네일은 비어있을 수 없습니다.") + String thumbnail, + @Schema(description = "여행기 일자 목록") + @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") + @Valid + List days +) { + + public Travelogue toTravelogue() { + return new Travelogue(title, thumbnail); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java new file mode 100644 index 00000000..bc81a312 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java @@ -0,0 +1,22 @@ +package woowacourse.touroot.travelogue.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Builder; +import woowacourse.touroot.travelogue.domain.TravelogueDay; + +@Builder +public record TravelogueDayResponse( + @Schema(description = "여행기 일자 ID", example = "1") + Long id, + @Schema(description = "여행기 장소 목록") + List places +) { + + public static TravelogueDayResponse of(TravelogueDay day, List places) { + return TravelogueDayResponse.builder() + .id(day.getId()) + .places(places) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java new file mode 100644 index 00000000..e3a18728 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java @@ -0,0 +1,21 @@ +package woowacourse.touroot.travelogue.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; + +@Builder +public record TravelogueLocationResponse( + @Schema(description = "여행기 장소 위도", example = "37.5175896") + String lat, + @Schema(description = "여행기 장소 설명", example = "127.0867236") + String lng +) { + + public static TravelogueLocationResponse from(TraveloguePlace place) { + return TravelogueLocationResponse.builder() + .lat(place.getLatitude()) + .lng(place.getLongitude()) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java new file mode 100644 index 00000000..ef8f20eb --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java @@ -0,0 +1,29 @@ +package woowacourse.touroot.travelogue.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.Builder; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; + +@Builder +public record TraveloguePlaceResponse( + @Schema(description = "여행기 장소 ID", example = "1") + Long id, + @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") + String name, + @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") + String description, + TravelogueLocationResponse location, + List photoUrls +) { + + public static TraveloguePlaceResponse of(TraveloguePlace place, List photoUrls) { + return TraveloguePlaceResponse.builder() + .id(place.getId()) + .name(place.getName()) + .description(place.getDescription()) + .location(TravelogueLocationResponse.from(place)) + .photoUrls(photoUrls) + .build(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueResponse.java similarity index 64% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java rename to backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueResponse.java index e9f44219..cfb554a4 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/TravelogueResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueResponse.java @@ -1,10 +1,11 @@ -package woowacourse.touroot.travelogue.dto; +package woowacourse.touroot.travelogue.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; import lombok.Builder; -import woowacourse.touroot.travelogue.domain.day.dto.TravelogueDayResponse; +import woowacourse.touroot.travelogue.domain.Travelogue; @Builder public record TravelogueResponse( @@ -19,6 +20,16 @@ public record TravelogueResponse( String thumbnail, @Schema(description = "여행기 일자 목록") @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") + @Valid List days ) { + + public static TravelogueResponse of(Travelogue travelogue, List days) { + return TravelogueResponse.builder() + .id(travelogue.getId()) + .title(travelogue.getTitle()) + .thumbnail(travelogue.getThumbnail()) + .days(days) + .build(); + } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueDayRepository.java similarity index 68% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java rename to backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueDayRepository.java index d480bf83..b1fe0a89 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/day/repository/TravelogueDayRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueDayRepository.java @@ -1,11 +1,11 @@ -package woowacourse.touroot.travelogue.domain.day.repository; +package woowacourse.touroot.travelogue.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TravelogueDay; public interface TravelogueDayRepository extends JpaRepository { - + List findByTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePhotoRepository.java similarity index 55% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java rename to backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePhotoRepository.java index a63e83a3..90f508a2 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/photo/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.domain.photo.repository; +package woowacourse.touroot.travelogue.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.domain.photo.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; public interface TraveloguePhotoRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePlaceRepository.java similarity index 54% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java rename to backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePlaceRepository.java index d6e886c4..f28af54d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/place/repsitory/TraveloguePlaceRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePlaceRepository.java @@ -1,11 +1,11 @@ -package woowacourse.touroot.travelogue.domain.place.repsitory; +package woowacourse.touroot.travelogue.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; public interface TraveloguePlaceRepository extends JpaRepository { - + List findByTravelogueDay(TravelogueDay travelogueDay); } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java new file mode 100644 index 00000000..cbc3ba1e --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java @@ -0,0 +1,48 @@ +package woowacourse.touroot.travelogue.service; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; + +@RequiredArgsConstructor +@Service +public class TravelogueDayService { + + private final TravelogueDayRepository travelogueDayRepository; + + @Transactional + public Map> createDays( + List requests, + Travelogue travelogue + ) { + Map> daysWithPlaceRequests = new LinkedHashMap<>(); + + for (int i = 0; i < requests.size(); i++) { + TravelogueDayRequest request = requests.get(i); + TravelogueDay travelogueDay = request.toTravelogueDay(i, travelogue); + daysWithPlaceRequests.put(travelogueDayRepository.save(travelogueDay), request.places()); + } + + return daysWithPlaceRequests; + } + + @Transactional(readOnly = true) + public List findDaysByTravelogue(Travelogue travelogue) { + return travelogueDayRepository.findByTravelogue(travelogue); + } + + @Transactional(readOnly = true) + public TravelogueDay findDayById(Long id) { + return travelogueDayRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 일자입니다.")); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java index d2418d36..3f261cbc 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java @@ -2,17 +2,23 @@ import java.util.Comparator; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.day.dto.TravelogueDayResponse; -import woowacourse.touroot.travelogue.domain.day.service.TravelogueDayService; -import woowacourse.touroot.travelogue.domain.photo.service.TraveloguePhotoService; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.domain.place.dto.TraveloguePlaceResponse; -import woowacourse.touroot.travelogue.domain.place.service.TraveloguePlaceService; -import woowacourse.touroot.travelogue.dto.TravelogueResponse; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; +import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; @RequiredArgsConstructor @Service @@ -23,15 +29,50 @@ public class TravelogueFacadeService { private final TraveloguePlaceService traveloguePlaceService; private final TraveloguePhotoService traveloguePhotoService; + public TravelogueResponse createTravelogue(TravelogueRequest request) { + Travelogue travelogue = travelogueService.createTravelogue(request); + + return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue)); + } + + private List createDays(List requests, Travelogue travelogue) { + Map> days = travelogueDayService.createDays(requests, travelogue); + + return days.keySet() + .stream() + .map(day -> TravelogueDayResponse.of(day, createPlaces(days.get(day), day))) + .toList(); + } + + private List createPlaces(List requests, TravelogueDay day) { + Map> places = traveloguePlaceService.createPlaces(requests, day); + + return places.keySet() + .stream() + .map(place -> TraveloguePlaceResponse.of(place, createPhotos(places.get(place), place))) + .toList(); + } + + private List createPhotos(List requests, TraveloguePlace place) { + List photos = traveloguePhotoService.createPhotos(requests, place); + + return photos.stream() + .map(TraveloguePhoto::getKey) + .toList(); + } + public TravelogueResponse findTravelogueById(Long id) { Travelogue travelogue = travelogueService.getTravelogueById(id); - return TravelogueResponse.builder() - .id(travelogue.getId()) - .title(travelogue.getTitle()) - .thumbnail(travelogue.getThumbnail()) - .days(findDaysOfTravelogue(travelogue)) - .build(); + return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue)); + } + + public Page findTravelogues(final Pageable pageable) { + Page travelogues = travelogueService.findAll(pageable); + + return new PageImpl<>(travelogues.stream() + .map(travelogue -> TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue))) + .toList()); } private List findDaysOfTravelogue(Travelogue travelogue) { @@ -39,37 +80,19 @@ private List findDaysOfTravelogue(Travelogue travelogue) return travelogueDays.stream() .sorted(Comparator.comparing(TravelogueDay::getOrder)) - .map(this::getTravelogueDayResponse) + .map(day -> TravelogueDayResponse.of(day, findPlacesOfTravelogueDay(day))) .toList(); } - private TravelogueDayResponse getTravelogueDayResponse(TravelogueDay day) { - return TravelogueDayResponse.builder() - .id(day.getId()) - .places(findPlacesOfTravelogueDay(day)) - .build(); - } - private List findPlacesOfTravelogueDay(TravelogueDay travelogueDay) { List places = traveloguePlaceService.findTraveloguePlaceByDay(travelogueDay); return places.stream() .sorted(Comparator.comparing(TraveloguePlace::getOrder)) - .map(this::getTraveloguePlaceResponse) + .map(place -> TraveloguePlaceResponse.of(place, findPhotoUrlsOfTraveloguePlace(place))) .toList(); } - private TraveloguePlaceResponse getTraveloguePlaceResponse(TraveloguePlace place) { - return TraveloguePlaceResponse.builder() - .id(place.getId()) - .name(place.getName()) - .description(place.getDescription()) - .lat(place.getLatitude()) - .lng(place.getLongitude()) - .photoUrls(findPhotoUrlsOfTraveloguePlace(place)) - .build(); - } - private List findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) { return traveloguePhotoService.findPhotoUrlsByPlace(place); } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java new file mode 100644 index 00000000..24b11db7 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java @@ -0,0 +1,42 @@ +package woowacourse.touroot.travelogue.service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; + +@RequiredArgsConstructor +@Service +public class TraveloguePhotoService { + + private final TraveloguePhotoRepository traveloguePhotoRepository; + + @Transactional + public List createPhotos(List requests, TraveloguePlace place) { + List photos = new ArrayList<>(); + + for (int i = 0; i < requests.size(); i++) { + TraveloguePhotoRequest request = requests.get(i); + TraveloguePhoto photo = request.toTraveloguePhoto(i, place); + photos.add(traveloguePhotoRepository.save(photo)); + } + + return photos; + } + + @Transactional(readOnly = true) + public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { + List photos = traveloguePhotoRepository.findByTraveloguePlace(traveloguePlace); + + return photos.stream() + .sorted(Comparator.comparing(TraveloguePhoto::getOrder)) + .map(TraveloguePhoto::getKey) + .toList(); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java new file mode 100644 index 00000000..fcb72b29 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java @@ -0,0 +1,61 @@ +package woowacourse.touroot.travelogue.service; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.place.repository.PlaceRepository; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; + +@RequiredArgsConstructor +@Service +public class TraveloguePlaceService { + + private final PlaceRepository placeRepository; + private final TraveloguePlaceRepository traveloguePlaceRepository; + + @Transactional + public Map> createPlaces( + List requests, + TravelogueDay day + ) { + Map> places = new LinkedHashMap<>(); + + for (int i = 0; i < requests.size(); i++) { + TraveloguePlaceRequest request = requests.get(i); + Place place = getPlace(request); + + TraveloguePlace traveloguePlace = request.toTraveloguePlace(i, place, day); + places.put(traveloguePlaceRepository.save(traveloguePlace), request.photos()); + } + + return places; + } + + private Place getPlace(TraveloguePlaceRequest request) { + return placeRepository.findByNameAndLatitudeAndLongitude( + request.name(), + request.location().lat(), + request.location().lng() + ).orElseGet(() -> placeRepository.save(request.toPlace())); + } + + @Transactional(readOnly = true) + public List findTraveloguePlaceByDay(TravelogueDay travelogueDay) { + return traveloguePlaceRepository.findByTravelogueDay(travelogueDay); + } + + @Transactional(readOnly = true) + public TraveloguePlace findTraveloguePlaceById(Long id) { + return traveloguePlaceRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 장소입니다.")); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java index 41fe3571..e9d805bb 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java @@ -1,10 +1,13 @@ package woowacourse.touroot.travelogue.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; import woowacourse.touroot.travelogue.repository.TravelogueRepository; @RequiredArgsConstructor @@ -13,9 +16,19 @@ public class TravelogueService { private final TravelogueRepository travelogueRepository; + @Transactional + public Travelogue createTravelogue(TravelogueRequest request) { + Travelogue travelogue = request.toTravelogue(); + return travelogueRepository.save(travelogue); + } + @Transactional(readOnly = true) public Travelogue getTravelogueById(Long id) { return travelogueRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기입니다.")); } + + public Page findAll(Pageable pageable) { + return travelogueRepository.findAll(pageable); + } } diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java index adfa3748..20d04067 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java @@ -43,4 +43,4 @@ void findTravelogue() { .statusCode(200) .body("title", is("여행기 1")); } -} \ No newline at end of file +} diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java index 7f2aaac9..18cd9122 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java @@ -1,16 +1,17 @@ package woowacourse.touroot.utils; +import java.time.LocalDate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.day.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.day.repository.TravelogueDayRepository; -import woowacourse.touroot.travelogue.domain.photo.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.photo.repository.TraveloguePhotoRepository; -import woowacourse.touroot.travelogue.domain.place.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.domain.place.repsitory.TraveloguePlaceRepository; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; +import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; +import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; import woowacourse.touroot.travelogue.repository.TravelogueRepository; import woowacourse.touroot.travelplan.domain.TravelPlan; import woowacourse.touroot.travelplan.domain.TravelPlanDay; @@ -19,8 +20,6 @@ import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; import woowacourse.touroot.travelplan.repository.TravelPlanRepository; -import java.time.LocalDate; - @Component public class TestFixture { @@ -65,7 +64,7 @@ public static TraveloguePlace getTraveloguePlace(Integer order, String descripti } public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { - return new TraveloguePhoto(key, order, traveloguePlace); + return new TraveloguePhoto(order, key, traveloguePlace); } public static TravelPlan getTravelPlan(String title, LocalDate startDate) { From bb8787d0ccfac34bef61ab9a3fd699be11b41849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:18:48 +0900 Subject: [PATCH 028/108] =?UTF-8?q?[Fix]=20-=20CI=20xml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=AF=B8=EC=9D=B8=EC=8B=9D=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9A=94=EC=B2=AD=20=EC=B4=88=EA=B3=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=95=A1=EC=85=98=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: CI 액션에 전달하는 xml 파일 경로 미인식 문제 해결 * chore: CI `publish-unit-test-result-action` 버전 업그레이드 * chore: CI `publish-unit-test-result-action`의 동시 요청 시간과 재시도 대기 시간 증가 * fix: CI xml 파일 경로 수정 --- .github/workflows/be-ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/be-ci.yml b/.github/workflows/be-ci.yml index 05ac48a5..9ea7397d 100644 --- a/.github/workflows/be-ci.yml +++ b/.github/workflows/be-ci.yml @@ -28,14 +28,17 @@ jobs: run: ./gradlew build - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 + uses: EnricoMi/publish-unit-test-result-action@v2 if: ${{ always() }} with: - files: ./build/test-results/**/*.xml + files: ${{ github.workspace }}/backend/build/test-results/**/*.xml + seconds_between_github_reads: 1.0 + seconds_between_github_writes: 3.0 + secondary_rate_limit_wait_seconds: 90.0 - name: When test fail, comment on that code uses: mikepenz/action-junit-report@v3 if: always() with: - report_paths: ./build/test-results/**/*.xml + report_paths: ${{ github.workspace }}/backend/build/test-results/**/*.xml token: ${{ github.token }} From 7333ddf14da134e2980a1e5efae02b3f564c3d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Mon, 22 Jul 2024 15:26:20 +0900 Subject: [PATCH 029/108] =?UTF-8?q?[Feature]=20-=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20(#8?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 로그인 응답에 데이터 추가, Response 생성 방식 캡슐화 - 카카오 닉네임, 프로필 이미지 경로 * refactor: RestClient 예외 처리 작성 * feat: 서버 내부 에러 처리 핸들러 작성 * feat: Swagger api 정보 추가 * fix: 최상위 예외 처리 코드 제거 개선 --- .../controller/LoginController.java | 17 +++++++++++++++++ .../authentication/dto/LoginResponse.java | 12 +++++++++++- .../infrastructure/KakaoOauthClient.java | 15 +++++++++++++++ .../authentication/service/LoginService.java | 4 ++-- .../global/exception/ClientException.java | 8 ++++++++ .../exception/GlobalExceptionHandler.java | 8 ++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java diff --git a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java index 40fc3fae..abb762b8 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java @@ -1,5 +1,10 @@ package woowacourse.touroot.authentication.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -8,7 +13,9 @@ import org.springframework.web.bind.annotation.RestController; import woowacourse.touroot.authentication.dto.LoginResponse; import woowacourse.touroot.authentication.service.LoginService; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; +@Tag(name = "로그인") @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/login") @@ -16,6 +23,16 @@ public class LoginController { private final LoginService loginService; + @Operation( + summary = "카카오 로그인", + responses = { + @ApiResponse( + responseCode = "400", + description = "유효하지 않은 인가 코드로 로그인 요청을 했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + } + ) @GetMapping("/oauth/kakao") public ResponseEntity login(@RequestParam(name = "code") String authorizationCode) { return ResponseEntity.ok() diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java index 156f99fc..a4cccb33 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java @@ -1,4 +1,14 @@ package woowacourse.touroot.authentication.dto; -public record LoginResponse(String accessToken) { +import io.swagger.v3.oas.annotations.media.Schema; +import woowacourse.touroot.member.domain.Member; + +public record LoginResponse( + @Schema(description = "로그인된 유저의 닉네임") String nickname, + @Schema(description = "로그인된 유저의 프로필 이미지 경로") String profileImageUrl, + @Schema(description = "인가에 필요한 accessToken") String accessToken) { + + public static LoginResponse of(Member member, String accessToken) { + return new LoginResponse(member.getNickname(), member.getProfileImageUri(), accessToken); + } } diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java index ccb2c1a3..461bea3b 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java @@ -1,18 +1,24 @@ package woowacourse.touroot.authentication.infrastructure; +import java.io.IOException; import java.time.Duration; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import woowacourse.touroot.authentication.dto.KakaoAccessTokenResponse; import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.global.exception.ClientException; @Component public class KakaoOauthClient { @@ -55,6 +61,7 @@ public OauthUserInformationResponse requestUserInformation(String authorizationC .uri(userInformationRequestUri) .header(HttpHeaders.AUTHORIZATION, "Bearer " + kakaoAccessTokenResponse.accessToken()) .retrieve() + .onStatus(HttpStatusCode::isError, this::handleClientError) .toEntity(OauthUserInformationResponse.class) .getBody(); } @@ -71,7 +78,15 @@ private KakaoAccessTokenResponse requestAccessToken(String authorizationCode) { .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(params) .retrieve() + .onStatus(HttpStatusCode::isError, this::handleClientError) .toEntity(KakaoAccessTokenResponse.class) .getBody(); } + + private void handleClientError(HttpRequest request, ClientHttpResponse response) throws IOException { + if (response.getStatusCode().is4xxClientError()) { + throw new BadRequestException("잘못된 로그인 요청입니다. 인가코드를 확인해주세요"); + } + throw new ClientException("외부 서비스의 장애로 카카오로그인을 이용할 수 없습니다"); + } } diff --git a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java b/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java index 9f02b4e7..abb1a04a 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java @@ -22,9 +22,9 @@ public LoginResponse login(String code) { Member member = memberRepository.findByKakaoId(userInformation.socialLoginId()) .orElseGet(() -> signUp(userInformation)); - return new LoginResponse(tokenProvider.createToken(member)); + return LoginResponse.of(member, tokenProvider.createToken(member)); } - + private Member signUp(OauthUserInformationResponse userInformation) { return memberRepository.save( new Member(userInformation.socialLoginId(), userInformation.nickname(), userInformation.profileImage()) diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java b/backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java new file mode 100644 index 00000000..5abf4cd8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java @@ -0,0 +1,8 @@ +package woowacourse.touroot.global.exception; + +public class ClientException extends RuntimeException { + + public ClientException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java index c8f0872e..6e2bd4f3 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java @@ -34,4 +34,12 @@ public ResponseEntity handleMethodArgumentNotValidException( return ResponseEntity.badRequest() .body(data); } + + @ExceptionHandler(ClientException.class) + public ResponseEntity handleClientException(ClientException exception) { + log.error("CLIENT_EXCEPTION :: message = {}", exception.getMessage()); + + ExceptionResponse data = new ExceptionResponse(exception.getMessage()); + return ResponseEntity.internalServerError().body(data); + } } From 0701d93d86fd63365bcff582db0cba2d6769afd9 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:47:35 +0900 Subject: [PATCH 030/108] [Feature] - dev profile https (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: applicaton.yml에 ssl 설정 추가 * feat: github action에 keystore.p12 만드는 작업 추가 --- .github/workflows/be-cd-dev.yml | 3 +++ .github/workflows/be-ci.yml | 3 +++ backend/src/main/resources/application.yml | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/be-cd-dev.yml b/.github/workflows/be-cd-dev.yml index 8a8bd27f..4bc39d4a 100644 --- a/.github/workflows/be-cd-dev.yml +++ b/.github/workflows/be-cd-dev.yml @@ -21,6 +21,9 @@ jobs: distribution: 'temurin' java-version: '17' + - name: Make keystore file + run: echo "${{secrets.SSL_KEYSTORE}}" | base64 --decode > ./src/main/resources/keystore.p12 + - name: Setup Gradle uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 diff --git a/.github/workflows/be-ci.yml b/.github/workflows/be-ci.yml index 9ea7397d..648c43e3 100644 --- a/.github/workflows/be-ci.yml +++ b/.github/workflows/be-ci.yml @@ -21,6 +21,9 @@ jobs: distribution: 'temurin' java-version: '17' + - name: Make keystore file + run: echo "${{secrets.SSL_KEYSTORE}}" | base64 --decode > ./src/main/resources/keystore.p12 + - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index abccd82d..bbc7fccd 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -42,6 +42,11 @@ security: token: secret-key: ENC(L36WWjoZtP2nHHkqxDGlYLsMHMp+EBL2Fnl+X2de2KHk+PIfViyVM7rCYcbcFpo7yB4MaP++atU=) expire-length: 1800000 +server: + ssl: + key-store-type: PKCS12 + key-store-password: ENC(aIkhk+PERnL/OBpa2HtNyXAjOGbCZx9TJ+L5sekBxdI=) + key-store: ENC(7VQCNdI7mXATwc4AiymZoyf3mz9SiskXpLnenpMSFBI=) spring: config: activate: From 1575c19cd20bff9ac1c69a87016ec8be1897e059 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 24 Jul 2024 16:29:26 +0900 Subject: [PATCH 031/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 불필요한 컨트롤러 제거 * refactor: Service 계층 @Transactional 시작 위치 변경 * feat: TestFixture 도메인별 분리 및 Helper로 역할 분리 * refactor: 컨트롤러 테스트 코드 개선 * test: Facade 서비스 테스트 작성 * refactor: id 기준 equals & hashCode 추가 * chore: API 문서에 응답 정보 추가 * chore: 메소드 이름과 예외 메시지 수정 * chore: Test fixture 값 수정 * refactor: Test용 엔티티 영속화 로직 수정 * test: 여행기 관련 서비스 계층 통합 테스트 작성 * fix: 테스트 격리를 위한 DB 초기화 로직 추가 * chore: EOL 추가 --- .../woowacourse/touroot/PingController.java | 15 --- .../controller/TravelogueController.java | 45 ++++++- .../touroot/travelogue/domain/Travelogue.java | 2 + .../travelogue/domain/TraveloguePhoto.java | 2 + .../travelogue/domain/TraveloguePlace.java | 2 + .../service/TravelogueDayService.java | 6 +- .../service/TravelogueFacadeService.java | 6 +- .../service/TraveloguePhotoService.java | 3 - .../service/TraveloguePlaceService.java | 8 +- .../travelogue/service/TravelogueService.java | 3 - .../touroot/global/ServiceTest.java | 13 ++ .../controller/TravelogueControllerTest.java | 74 +++++++++-- .../fixture/TravelogueTestFixture.java | 120 ++++++++++++++++++ .../helper/TravelogueTestHelper.java | 99 +++++++++++++++ .../service/TravelogueDayServiceTest.java | 89 +++++++++++++ .../service/TravelogueFacadeServiceTest.java | 69 ++++++++++ .../service/TraveloguePhotoServiceTest.java | 59 +++++++++ .../service/TraveloguePlaceServiceTest.java | 94 ++++++++++++++ .../service/TravelogueServiceTest.java | 83 ++++++++++++ .../service/TravelPlanServiceTest.java | 23 ++-- .../touroot/utils/TestFixture.java | 51 -------- 21 files changed, 760 insertions(+), 106 deletions(-) delete mode 100644 backend/src/main/java/woowacourse/touroot/PingController.java create mode 100644 backend/src/test/java/woowacourse/touroot/global/ServiceTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java diff --git a/backend/src/main/java/woowacourse/touroot/PingController.java b/backend/src/main/java/woowacourse/touroot/PingController.java deleted file mode 100644 index 9d9e0585..00000000 --- a/backend/src/main/java/woowacourse/touroot/PingController.java +++ /dev/null @@ -1,15 +0,0 @@ -package woowacourse.touroot; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/ping") -public class PingController { - - @GetMapping - public String ping() { - return "pong"; - } -} diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java index 57fd67c8..0f689ee0 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java @@ -2,6 +2,10 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.net.URI; @@ -18,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; import woowacourse.touroot.travelogue.service.TravelogueFacadeService; @@ -30,7 +35,19 @@ public class TravelogueController { private final TravelogueFacadeService travelogueFacadeService; - @Operation(description = "여행기 작성") + + @Operation(summary = "여행기 작성") + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 값이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) @PostMapping public ResponseEntity createTravelogue(@Valid @RequestBody TravelogueRequest request) { TravelogueResponse response = travelogueFacadeService.createTravelogue(request); @@ -39,13 +56,35 @@ public ResponseEntity createTravelogue(@Valid @RequestBody T .body(response); } - @Operation(description = "여행기 상세 조회") + @Operation(summary = "여행기 상세 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 여행기 ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) @GetMapping("/{id}") public ResponseEntity findTravelogue(@Valid @PathVariable Long id) { return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id)); } - @Operation(description = "여행기 메인 페이지 조회") + @Operation(summary = "여행기 메인 페이지 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "올바르지 않은 페이지네이션 옵션으로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) @PageableAsQueryParam @GetMapping public ResponseEntity> findMainPageTravelogues( diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java index 045a53d2..18856a22 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java @@ -7,11 +7,13 @@ import jakarta.persistence.Id; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; @Getter +@EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java index 38d64560..31b71b9d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java @@ -10,11 +10,13 @@ import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; @Getter +@EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java index b09d2ac5..670abd98 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java @@ -10,12 +10,14 @@ import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import woowacourse.touroot.entity.BaseEntity; import woowacourse.touroot.place.domain.Place; @Getter +@EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java index cbc3ba1e..3e58305a 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java @@ -5,7 +5,6 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelogue.domain.Travelogue; import woowacourse.touroot.travelogue.domain.TravelogueDay; @@ -19,7 +18,6 @@ public class TravelogueDayService { private final TravelogueDayRepository travelogueDayRepository; - @Transactional public Map> createDays( List requests, Travelogue travelogue @@ -35,14 +33,12 @@ public Map> createDays( return daysWithPlaceRequests; } - @Transactional(readOnly = true) public List findDaysByTravelogue(Travelogue travelogue) { return travelogueDayRepository.findByTravelogue(travelogue); } - @Transactional(readOnly = true) public TravelogueDay findDayById(Long id) { return travelogueDayRepository.findById(id) - .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 일자입니다.")); + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기 일자입니다.")); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java index 3f261cbc..a1db74a5 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import woowacourse.touroot.travelogue.domain.Travelogue; import woowacourse.touroot.travelogue.domain.TravelogueDay; import woowacourse.touroot.travelogue.domain.TraveloguePhoto; @@ -29,6 +30,7 @@ public class TravelogueFacadeService { private final TraveloguePlaceService traveloguePlaceService; private final TraveloguePhotoService traveloguePhotoService; + @Transactional public TravelogueResponse createTravelogue(TravelogueRequest request) { Travelogue travelogue = travelogueService.createTravelogue(request); @@ -61,12 +63,14 @@ private List createPhotos(List requests, Travelo .toList(); } + @Transactional(readOnly = true) public TravelogueResponse findTravelogueById(Long id) { Travelogue travelogue = travelogueService.getTravelogueById(id); return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue)); } + @Transactional(readOnly = true) public Page findTravelogues(final Pageable pageable) { Page travelogues = travelogueService.findAll(pageable); @@ -85,7 +89,7 @@ private List findDaysOfTravelogue(Travelogue travelogue) } private List findPlacesOfTravelogueDay(TravelogueDay travelogueDay) { - List places = traveloguePlaceService.findTraveloguePlaceByDay(travelogueDay); + List places = traveloguePlaceService.findTraveloguePlacesByDay(travelogueDay); return places.stream() .sorted(Comparator.comparing(TraveloguePlace::getOrder)) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java index 24b11db7..8fbe27df 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java @@ -5,7 +5,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import woowacourse.touroot.travelogue.domain.TraveloguePhoto; import woowacourse.touroot.travelogue.domain.TraveloguePlace; import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; @@ -17,7 +16,6 @@ public class TraveloguePhotoService { private final TraveloguePhotoRepository traveloguePhotoRepository; - @Transactional public List createPhotos(List requests, TraveloguePlace place) { List photos = new ArrayList<>(); @@ -30,7 +28,6 @@ public List createPhotos(List requests, return photos; } - @Transactional(readOnly = true) public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { List photos = traveloguePhotoRepository.findByTraveloguePlace(traveloguePlace); diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java index fcb72b29..d82acffe 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java @@ -5,7 +5,6 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; @@ -22,7 +21,6 @@ public class TraveloguePlaceService { private final PlaceRepository placeRepository; private final TraveloguePlaceRepository traveloguePlaceRepository; - @Transactional public Map> createPlaces( List requests, TravelogueDay day @@ -48,14 +46,12 @@ private Place getPlace(TraveloguePlaceRequest request) { ).orElseGet(() -> placeRepository.save(request.toPlace())); } - @Transactional(readOnly = true) - public List findTraveloguePlaceByDay(TravelogueDay travelogueDay) { + public List findTraveloguePlacesByDay(TravelogueDay travelogueDay) { return traveloguePlaceRepository.findByTravelogueDay(travelogueDay); } - @Transactional(readOnly = true) public TraveloguePlace findTraveloguePlaceById(Long id) { return traveloguePlaceRepository.findById(id) - .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 장소입니다.")); + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기 장소입니다.")); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java index e9d805bb..cfeb160c 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java @@ -4,7 +4,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelogue.domain.Travelogue; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; @@ -16,13 +15,11 @@ public class TravelogueService { private final TravelogueRepository travelogueRepository; - @Transactional public Travelogue createTravelogue(TravelogueRequest request) { Travelogue travelogue = request.toTravelogue(); return travelogueRepository.save(travelogue); } - @Transactional(readOnly = true) public Travelogue getTravelogueById(Long id) { return travelogueRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기입니다.")); diff --git a/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java b/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java new file mode 100644 index 00000000..954cc446 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java @@ -0,0 +1,13 @@ +package woowacourse.touroot.global; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@Retention(RetentionPolicy.RUNTIME) +@TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) +public @interface ServiceTest { +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java index 20d04067..32132ca5 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java @@ -2,6 +2,8 @@ import static org.hamcrest.Matchers.is; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.ContentType; import org.junit.jupiter.api.BeforeEach; @@ -9,38 +11,92 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.data.domain.Page; import woowacourse.touroot.global.AcceptanceTest; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; -import woowacourse.touroot.utils.TestFixture; +@DisplayName("여행기 컨트롤러") @AcceptanceTest class TravelogueControllerTest { @LocalServerPort private int port; + private final DatabaseCleaner databaseCleaner; + private final TravelogueTestHelper testHelper; + private final ObjectMapper objectMapper; @Autowired - private DatabaseCleaner databaseCleaner; - - @Autowired - private TestFixture testFixture; + public TravelogueControllerTest( + DatabaseCleaner databaseCleaner, + TravelogueTestHelper testHelper, + ObjectMapper objectMapper + ) { + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + this.objectMapper = objectMapper; + } @BeforeEach void setUp() { RestAssured.port = port; databaseCleaner.executeTruncate(); - testFixture.initTravelogueTestData(); + } + + @DisplayName("여행기를 작성한다.") + @Test + void createTravelogue() { + TravelogueRequest request = TravelogueTestFixture.getTravelogueRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/travelogues") + .then().log().all() + .statusCode(201) + .header("Location", "/api/v1/travelogues/1"); } @DisplayName("여행기를 상세 조회한다.") @Test - void findTravelogue() { + void findTravelogue() throws JsonProcessingException { + testHelper.initTravelogueTestData(); + TravelogueResponse response = TravelogueTestFixture.getTravelogueResponse(); + + RestAssured.given().log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues/1") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + + @DisplayName("메인 페이지 조회 시, 최신 작성 순으로 여행기를 조회한다.") + @Test + void findMainPageTravelogues() throws JsonProcessingException { + testHelper.initTravelogueTestData(); + Page responses = TravelogueTestFixture.getTravelogueResponses(); + + RestAssured.given().log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(responses))); + } + + @DisplayName("존재하지 않는 여행기를 조회하면 예외가 발생한다.") + @Test + void findNotExistTravelogueThrowException() { RestAssured.given().log().all() .accept(ContentType.JSON) .when().get("/api/v1/travelogues/1") .then().log().all() - .statusCode(200) - .body("title", is("여행기 1")); + .statusCode(400).assertThat() + .body("message", is("존재하지 않는 여행기입니다.")); } } diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java new file mode 100644 index 00000000..d5da7601 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java @@ -0,0 +1,120 @@ +package woowacourse.touroot.travelogue.fixture; + +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.stereotype.Component; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; +import woowacourse.touroot.travelogue.dto.request.TravelogueLocationRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; +import woowacourse.touroot.travelogue.dto.response.TravelogueLocationResponse; +import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; + +@Component +public class TravelogueTestFixture { + + private TravelogueTestFixture() { + } + + public static Travelogue getTravelogue() { + return new Travelogue("낭만의 시베리아 횡단철도 여행", "https://photo-key.jpeg"); + } + + public static TravelogueDay getTravelogueDay() { + return new TravelogueDay(1, getTravelogue()); + } + + public static Place getPlace() { + return new Place("블라디보스토크", "37.1234", "127.1234", ""); + } + + public static TraveloguePlace getTraveloguePlace() { + return new TraveloguePlace(1, "극동의 진주, 블라디보스토크.", getPlace(), getTravelogueDay()); + } + + public static TraveloguePhoto getTraveloguePhoto() { + return new TraveloguePhoto(1, "https://photo-key.jpeg", getTraveloguePlace()); + } + + public static TravelogueRequest getTravelogueRequest() { + return new TravelogueRequest("낭만의 시베리아 횡단철도 여행", "https://photo-key.jpeg", getTravelogueDayRequests()); + } + + public static List getTravelogueDayRequests() { + return List.of(new TravelogueDayRequest(getTraveloguePlaceRequests())); + } + + public static List getTraveloguePlaceRequests() { + return List.of(new TraveloguePlaceRequest( + "블라디보스토크", + getTravelogueLocationRequest(), + "극동의 진주, 블라디보스토크.", + getTraveloguePhotoRequests() + )); + } + + public static TravelogueLocationRequest getTravelogueLocationRequest() { + return new TravelogueLocationRequest("37.1234", "127.1234"); + } + + public static List getTraveloguePhotoRequests() { + return List.of(new TraveloguePhotoRequest("https://photo-key.jpeg")); + } + + public static TravelogueResponse getTravelogueResponse() { + return TravelogueResponse.builder() + .id(1L) + .title("낭만의 시베리아 횡단철도 여행") + .thumbnail("https://photo-key.jpeg") + .days(getTravelogueDayResponses()) + .build(); + } + + public static Page getTravelogueResponses() { + return new PageImpl<>(List.of(TravelogueResponse.builder() + .id(1L) + .title("낭만의 시베리아 횡단철도 여행") + .thumbnail("https://photo-key.jpeg") + .days(getTravelogueDayResponses()) + .build())); + } + + public static List getTravelogueDayResponses() { + return List.of(TravelogueDayResponse.builder() + .id(1L) + .places(getTraveloguePlaceResponses()) + .build() + ); + } + + public static List getTraveloguePlaceResponses() { + return List.of(TraveloguePlaceResponse.builder() + .id(1L) + .name("블라디보스토크") + .description("극동의 진주, 블라디보스토크.") + .location(getTravelogueLocationResponse()) + .photoUrls(getTraveloguePhotoUrls()) + .build() + ); + } + + public static TravelogueLocationResponse getTravelogueLocationResponse() { + return TravelogueLocationResponse.builder() + .lat("37.1234") + .lng("127.1234") + .build(); + } + + public static List getTraveloguePhotoUrls() { + return List.of("https://photo-key.jpeg"); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java new file mode 100644 index 00000000..5e24a140 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java @@ -0,0 +1,99 @@ +package woowacourse.touroot.travelogue.helper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.place.repository.PlaceRepository; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; +import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; +import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; +import woowacourse.touroot.travelogue.repository.TravelogueRepository; + +@Component +public class TravelogueTestHelper { + + private final PlaceRepository placeRepository; + private final TravelogueRepository travelogueRepository; + private final TravelogueDayRepository travelogueDayRepository; + private final TraveloguePlaceRepository traveloguePlaceRepository; + private final TraveloguePhotoRepository traveloguePhotoRepository; + + @Autowired + public TravelogueTestHelper( + PlaceRepository placeRepository, + TravelogueRepository travelogueRepository, + TravelogueDayRepository travelogueDayRepository, + TraveloguePlaceRepository traveloguePlaceRepository, + TraveloguePhotoRepository traveloguePhotoRepository + ) { + this.placeRepository = placeRepository; + this.travelogueRepository = travelogueRepository; + this.travelogueDayRepository = travelogueDayRepository; + this.traveloguePlaceRepository = traveloguePlaceRepository; + this.traveloguePhotoRepository = traveloguePhotoRepository; + } + + public static Travelogue getTravelogue(String name, String thumbnail) { + return new Travelogue(name, thumbnail); + } + + public static TravelogueDay getTravelogueDay(Integer order, Travelogue travelogue) { + return new TravelogueDay(order, travelogue); + } + + public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { + return new Place(name, latitude, longitude, googlePlaceId); + } + + public static TraveloguePlace getTraveloguePlace(Integer order, String description, Place place, + TravelogueDay travelogueDay) { + return new TraveloguePlace(order, description, place, travelogueDay); + } + + public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { + return new TraveloguePhoto(order, key, traveloguePlace); + } + + public void initTravelogueTestData() { + Travelogue travelogue = persistTravelogue(); + TravelogueDay day = persistTravelogueDay(travelogue); + Place location = persistPlace(); + TraveloguePlace place = persistTraveloguePlace(location, day); + persistTraveloguePhoto(place); + } + + public Travelogue persistTravelogue() { + Travelogue travelogue = TravelogueTestFixture.getTravelogue(); + + return travelogueRepository.save(travelogue); + } + + public TravelogueDay persistTravelogueDay(Travelogue travelogue) { + TravelogueDay day = TravelogueTestHelper.getTravelogueDay(1, travelogue); + + return travelogueDayRepository.save(day); + } + + public Place persistPlace() { + Place place = TravelogueTestFixture.getPlace(); + + return placeRepository.save(place); + } + + public TraveloguePlace persistTraveloguePlace(Place location, TravelogueDay day) { + TraveloguePlace place = getTraveloguePlace(1, "극동의 진주, 블라디보스토크.", location, day); + + return traveloguePlaceRepository.save(place); + } + + public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { + TraveloguePhoto photo = getTraveloguePhoto("https://photo-key.jpeg", 1, place); + + return traveloguePhotoRepository.save(photo); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java new file mode 100644 index 00000000..0531b5e0 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -0,0 +1,89 @@ +package woowacourse.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import java.util.Map; +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 woowacourse.touroot.global.ServiceTest; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; +import woowacourse.touroot.utils.DatabaseCleaner; + +@DisplayName("여행기 일자 서비스") +@ServiceTest +class TravelogueDayServiceTest { + + private final TravelogueDayService dayService; + private final DatabaseCleaner databaseCleaner; + private final TravelogueTestHelper testHelper; + + @Autowired + public TravelogueDayServiceTest( + TravelogueDayService dayService, + DatabaseCleaner databaseCleaner, + TravelogueTestHelper testHelper + ) { + this.dayService = dayService; + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @DisplayName("여행기의 일자들을 생성한다.") + @Test + void createDays() { + List requests = TravelogueTestFixture.getTravelogueDayRequests(); + Travelogue travelogue = testHelper.persistTravelogue(); + + Map> daysMap = dayService.createDays(requests, travelogue); + List days = daysMap.keySet().stream().toList(); + + assertAll( + () -> assertThat(daysMap.keySet()).hasSize(requests.size()), + () -> assertThat(daysMap).containsEntry(days.get(0), requests.get(0).places()) + ); + } + + @DisplayName("여행기를 기준으로 여행 일자들을 조회한다.") + @Test + void findDaysByTravelogue() { + Travelogue travelogue = testHelper.persistTravelogue(); + TravelogueDay travelogueDay = testHelper.persistTravelogueDay(travelogue); + + List days = dayService.findDaysByTravelogue(travelogue); + + assertThat(days).contains(travelogueDay); + } + + @DisplayName("여행 일자를 ID를 기준으로 조회한다.") + @Test + void findDayById() { + testHelper.initTravelogueTestData(); + + assertDoesNotThrow(() -> dayService.findDayById(1L)); + } + + @DisplayName("존재하지 않는 여행기 일자 ID로 조회하면 예외가 발생한다.") + @Test + void findDayByInvalidIdThrowException() { + assertThatThrownBy(() -> dayService.findDayById(1L)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기 일자입니다."); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java new file mode 100644 index 00000000..a0fb4884 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -0,0 +1,69 @@ +package woowacourse.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; + +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.data.domain.Page; +import org.springframework.data.domain.Pageable; +import woowacourse.touroot.global.ServiceTest; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; +import woowacourse.touroot.utils.DatabaseCleaner; + +@DisplayName("여행기 Facade 서비스") +@ServiceTest +class TravelogueFacadeServiceTest { + + private final TravelogueFacadeService service; + private final TravelogueTestHelper testHelper; + private final DatabaseCleaner databaseCleaner; + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @Autowired + public TravelogueFacadeServiceTest( + TravelogueFacadeService travelogueFacadeService, + TravelogueTestHelper travelogueTestHelper, + DatabaseCleaner databaseCleaner + ) { + this.service = travelogueFacadeService; + this.testHelper = travelogueTestHelper; + this.databaseCleaner = databaseCleaner; + } + + @DisplayName("여행기를 생성할 수 있다.") + @Test + void createTravelogue() { + TravelogueRequest request = TravelogueTestFixture.getTravelogueRequest(); + + assertThat(service.createTravelogue(request)) + .isEqualTo(TravelogueTestFixture.getTravelogueResponse()); + } + + @DisplayName("여행기를 ID를 기준으로 조회한다.") + @Test + void findTravelogueById() { + testHelper.initTravelogueTestData(); + + assertThat(service.findTravelogueById(1L)) + .isEqualTo(TravelogueTestFixture.getTravelogueResponse()); + } + + @DisplayName("메인 페이지에 표시할 여행기 목록을 조회한다.") + @Test + void findTravelogues() { + testHelper.initTravelogueTestData(); + Page responses = TravelogueTestFixture.getTravelogueResponses(); + + assertThat(service.findTravelogues(Pageable.ofSize(5))) + .isEqualTo(responses); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java new file mode 100644 index 00000000..ec8b9783 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -0,0 +1,59 @@ +package woowacourse.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import woowacourse.touroot.global.ServiceTest; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; + +@DisplayName("여행기 사진 서비스") +@ServiceTest +class TraveloguePhotoServiceTest { + + private final TraveloguePhotoService photoService; + private final TravelogueTestHelper testHelper; + + @Autowired + public TraveloguePhotoServiceTest(TraveloguePhotoService photoService, TravelogueTestHelper testHelper) { + this.photoService = photoService; + this.testHelper = testHelper; + } + + @DisplayName("여행기 사진을 생성한다.") + @Test + void createPhotos() { + List requests = TravelogueTestFixture.getTraveloguePhotoRequests(); + Travelogue travelogue = testHelper.persistTravelogue(); + TravelogueDay day = testHelper.persistTravelogueDay(travelogue); + Place location = testHelper.persistPlace(); + TraveloguePlace place = testHelper.persistTraveloguePlace(location, day); + + List photos = photoService.createPhotos(requests, place); + + assertThat(photos).hasSize(requests.size()); + } + + @DisplayName("여행기 사진 URL을 여행기 장소를 기준으로 조회한다.") + @Test + void findPhotoUrlsByPlace() { + Travelogue travelogue = testHelper.persistTravelogue(); + TravelogueDay day = testHelper.persistTravelogueDay(travelogue); + Place location = testHelper.persistPlace(); + TraveloguePlace place = testHelper.persistTraveloguePlace(location, day); + TraveloguePhoto photo = testHelper.persistTraveloguePhoto(place); + + List photoUrls = photoService.findPhotoUrlsByPlace(place); + + assertThat(photoUrls).contains(photo.getKey()); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java new file mode 100644 index 00000000..8bc88ce4 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -0,0 +1,94 @@ +package woowacourse.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import java.util.Map; +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 woowacourse.touroot.global.ServiceTest; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; +import woowacourse.touroot.utils.DatabaseCleaner; + +@DisplayName("여행기 장소 서비스") +@ServiceTest +class TraveloguePlaceServiceTest { + + private final TraveloguePlaceService placeService; + private final DatabaseCleaner databaseCleaner; + private final TravelogueTestHelper testHelper; + + @Autowired + public TraveloguePlaceServiceTest( + TraveloguePlaceService placeService, + DatabaseCleaner databaseCleaner, + TravelogueTestHelper testHelper + ) { + this.placeService = placeService; + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @DisplayName("여행기 장소를 생성한다.") + @Test + void createPlaces() { + List requests = TravelogueTestFixture.getTraveloguePlaceRequests(); + Travelogue travelogue = testHelper.persistTravelogue(); + TravelogueDay day = testHelper.persistTravelogueDay(travelogue); + + Map> placesMap = placeService.createPlaces(requests, day); + List places = placesMap.keySet().stream().toList(); + + assertAll( + () -> assertThat(placesMap.keySet()).hasSize(requests.size()), + () -> assertThat(placesMap).containsEntry(places.get(0), requests.get(0).photos()) + ); + } + + @DisplayName("여행기 장소를 여행기 일자를 기준으로 조회한다.") + @Test + void findTraveloguePlacesByDay() { + Travelogue travelogue = testHelper.persistTravelogue(); + TravelogueDay day = testHelper.persistTravelogueDay(travelogue); + Place location = testHelper.persistPlace(); + TraveloguePlace place = testHelper.persistTraveloguePlace(location, day); + + List places = placeService.findTraveloguePlacesByDay(day); + + assertThat(places).contains(place); + } + + @DisplayName("여행기 장소를 ID를 기준으로 조회한다.") + @Test + void findTraveloguePlaceById() { + testHelper.initTravelogueTestData(); + + assertDoesNotThrow(() -> placeService.findTraveloguePlaceById(1L)); + } + + @DisplayName("존재하지 않는 여행기 장소 ID로 조회하면 예외가 발생한다.") + @Test + void findDayByInvalidIdThrowException() { + assertThatThrownBy(() -> placeService.findTraveloguePlaceById(1L)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기 장소입니다."); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java new file mode 100644 index 00000000..d44e4ded --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java @@ -0,0 +1,83 @@ +package woowacourse.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +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.data.domain.Pageable; +import woowacourse.touroot.global.ServiceTest; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; +import woowacourse.touroot.utils.DatabaseCleaner; + +@DisplayName("여행기 서비스") +@ServiceTest +class TravelogueServiceTest { + + public static final int BASIC_PAGE_SIZE = 5; + + private final TravelogueService travelogueService; + private final DatabaseCleaner databaseCleaner; + private final TravelogueTestHelper testHelper; + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @Autowired + public TravelogueServiceTest( + TravelogueService travelogueService, + DatabaseCleaner databaseCleaner, + TravelogueTestHelper testHelper + ) { + this.travelogueService = travelogueService; + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @DisplayName("여행기를 생성할 수 있다.") + @Test + void createTravelogue() { + TravelogueRequest request = TravelogueTestFixture.getTravelogueRequest(); + Travelogue createdTravelogue = travelogueService.createTravelogue(request); + + assertAll( + () -> assertThat(createdTravelogue.getId()).isEqualTo(1L), + () -> assertThat(createdTravelogue.getTitle()).isEqualTo("낭만의 시베리아 횡단철도 여행") + ); + } + + @DisplayName("여행기는 ID를 기준으로 조회할 수 있다.") + @Test + void getTravelogueById() { + testHelper.initTravelogueTestData(); + + assertDoesNotThrow(() -> travelogueService.getTravelogueById(1L)); + } + + @DisplayName("존재하지 않는 ID로 여행기를 조회하면 예외가 발생한다.") + @Test + void getTravelogueByNotExistsIdThrowException() { + assertThatThrownBy(() -> travelogueService.getTravelogueById(1L)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + + @DisplayName("여행기를 전체 조회할 수 있다.") + @Test + void findAll() { + testHelper.initTravelogueTestData(); + + assertThat(travelogueService.findAll(Pageable.ofSize(BASIC_PAGE_SIZE))) + .hasSize(1); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java index ccbcb296..c8a54b05 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java @@ -1,10 +1,15 @@ package woowacourse.touroot.travelplan.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import java.util.List; +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 woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; @@ -15,16 +20,9 @@ import woowacourse.touroot.utils.DatabaseCleaner; import woowacourse.touroot.utils.TestFixture; -import java.time.LocalDate; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - @DisplayName("여행 계획 서비스") -@ActiveProfiles("test") // TODO: 양방향 해결 후 @DataJpaTest로 변경 -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@ServiceTest class TravelPlanServiceTest { private final TravelPlanService travelPlanService; @@ -42,6 +40,11 @@ public TravelPlanServiceTest( this.testFixture = testFixture; } + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + @DisplayName("여행 계획 서비스는 여행 계획 생성 시 생성된 id를 응답한다.") @Test void createTravelPlan() { diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java index 18cd9122..9567f368 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java @@ -5,14 +5,6 @@ import org.springframework.stereotype.Component; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; -import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; -import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; -import woowacourse.touroot.travelogue.repository.TravelogueRepository; import woowacourse.touroot.travelplan.domain.TravelPlan; import woowacourse.touroot.travelplan.domain.TravelPlanDay; import woowacourse.touroot.travelplan.domain.TravelPlanPlace; @@ -23,18 +15,6 @@ @Component public class TestFixture { - @Autowired - TravelogueRepository travelogueRepository; - - @Autowired - TravelogueDayRepository travelogueDayRepository; - - @Autowired - TraveloguePlaceRepository traveloguePlaceRepository; - - @Autowired - TraveloguePhotoRepository traveloguePhotoRepository; - @Autowired private PlaceRepository placeRepository; @@ -46,27 +26,10 @@ public class TestFixture { private TravelPlanPlaceRepository travelPlanPlaceRepository; - public static Travelogue getTravelogue(String name, String thumbnail) { - return new Travelogue(name, thumbnail); - } - - public static TravelogueDay getTravelogueDay(Integer order, Travelogue travelogue) { - return new TravelogueDay(order, travelogue); - } - public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { return new Place(name, latitude, longitude, googlePlaceId); } - public static TraveloguePlace getTraveloguePlace(Integer order, String description, Place place, - TravelogueDay travelogueDay) { - return new TraveloguePlace(order, description, place, travelogueDay); - } - - public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { - return new TraveloguePhoto(order, key, traveloguePlace); - } - public static TravelPlan getTravelPlan(String title, LocalDate startDate) { return new TravelPlan(title, startDate); } @@ -79,20 +42,6 @@ public static TravelPlanPlace getTravelPlanPlace(String description, int order, return new TravelPlanPlace(description, order, day, place); } - public void initTravelogueTestData() { - Travelogue travelogue = getTravelogue("여행기 1", "썸네일.png"); - TravelogueDay travelogueDay = getTravelogueDay(1, travelogue); - Place place = getPlace("장소 1", "33.3333", "127.2727", ""); - TraveloguePlace traveloguePlace = getTraveloguePlace(1, "좋은 장소", place, travelogueDay); - TraveloguePhoto traveloguePhoto = getTraveloguePhoto("image", 1, traveloguePlace); - - travelogueRepository.save(travelogue); - travelogueDayRepository.save(travelogueDay); - placeRepository.save(place); - traveloguePlaceRepository.save(traveloguePlace); - traveloguePhotoRepository.save(traveloguePhoto); - } - public void initTravelPlanTestData() { TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); From aaa87ab12bbd5cbe2e40831725c6e4447eaaf420 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 24 Jul 2024 17:11:41 +0900 Subject: [PATCH 032/108] =?UTF-8?q?[Refactor]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/TraveloguePlaceRequest.java | 4 ++-- ...quest.java => TraveloguePositionRequest.java} | 2 +- .../dto/response/TraveloguePlaceResponse.java | 4 ++-- ...onse.java => TraveloguePositionResponse.java} | 8 ++++---- .../service/TraveloguePlaceService.java | 4 ++-- .../fixture/TravelogueTestFixture.java | 16 ++++++++-------- .../travelogue/helper/TravelogueTestHelper.java | 8 ++++---- .../service/TraveloguePhotoServiceTest.java | 8 ++++---- .../service/TraveloguePlaceServiceTest.java | 4 ++-- 9 files changed, 29 insertions(+), 29 deletions(-) rename backend/src/main/java/woowacourse/touroot/travelogue/dto/request/{TravelogueLocationRequest.java => TraveloguePositionRequest.java} (92%) rename backend/src/main/java/woowacourse/touroot/travelogue/dto/response/{TravelogueLocationResponse.java => TraveloguePositionResponse.java} (76%) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index fe6c92a1..c449974e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -15,7 +15,7 @@ public record TraveloguePlaceRequest( @Schema(description = "여행기 장소 위치 정보") @NotNull(message = "여행기 장소 위치 정보는 비어있을 수 없습니다.") @Valid - TravelogueLocationRequest location, + TraveloguePositionRequest position, @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") String description, @Schema(description = "여행기 장소 사진") @@ -29,6 +29,6 @@ public TraveloguePlace toTraveloguePlace(int order, Place place, TravelogueDay t } public Place toPlace() { - return new Place(name, location.lat(), location.lng()); + return new Place(name, position.lat(), position.lng()); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePositionRequest.java similarity index 92% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java rename to backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePositionRequest.java index 9f995d1f..a71d690b 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueLocationRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePositionRequest.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -public record TravelogueLocationRequest( +public record TraveloguePositionRequest( @Schema(description = "여행기 장소 위도", example = "37.5175896") @NotNull(message = "여행기 장소 위도는 비어있을 수 없습니다.") String lat, diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java index ef8f20eb..f80a1757 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java @@ -13,7 +13,7 @@ public record TraveloguePlaceResponse( String name, @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") String description, - TravelogueLocationResponse location, + TraveloguePositionResponse position, List photoUrls ) { @@ -22,7 +22,7 @@ public static TraveloguePlaceResponse of(TraveloguePlace place, List pho .id(place.getId()) .name(place.getName()) .description(place.getDescription()) - .location(TravelogueLocationResponse.from(place)) + .position(TraveloguePositionResponse.from(place)) .photoUrls(photoUrls) .build(); } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePositionResponse.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java rename to backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePositionResponse.java index e3a18728..dc26d2c6 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueLocationResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePositionResponse.java @@ -5,15 +5,15 @@ import woowacourse.touroot.travelogue.domain.TraveloguePlace; @Builder -public record TravelogueLocationResponse( +public record TraveloguePositionResponse( @Schema(description = "여행기 장소 위도", example = "37.5175896") String lat, @Schema(description = "여행기 장소 설명", example = "127.0867236") String lng ) { - - public static TravelogueLocationResponse from(TraveloguePlace place) { - return TravelogueLocationResponse.builder() + + public static TraveloguePositionResponse from(TraveloguePlace place) { + return TraveloguePositionResponse.builder() .lat(place.getLatitude()) .lng(place.getLongitude()) .build(); diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java index d82acffe..441e4844 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java @@ -41,8 +41,8 @@ public Map> createPlaces( private Place getPlace(TraveloguePlaceRequest request) { return placeRepository.findByNameAndLatitudeAndLongitude( request.name(), - request.location().lat(), - request.location().lng() + request.position().lat(), + request.position().lng() ).orElseGet(() -> placeRepository.save(request.toPlace())); } diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java index d5da7601..87c0a830 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java @@ -10,13 +10,13 @@ import woowacourse.touroot.travelogue.domain.TraveloguePhoto; import woowacourse.touroot.travelogue.domain.TraveloguePlace; import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; -import woowacourse.touroot.travelogue.dto.request.TravelogueLocationRequest; import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePositionRequest; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; -import woowacourse.touroot.travelogue.dto.response.TravelogueLocationResponse; import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import woowacourse.touroot.travelogue.dto.response.TraveloguePositionResponse; import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; @Component @@ -56,14 +56,14 @@ public static List getTravelogueDayRequests() { public static List getTraveloguePlaceRequests() { return List.of(new TraveloguePlaceRequest( "블라디보스토크", - getTravelogueLocationRequest(), + getTraveloguePositionRequest(), "극동의 진주, 블라디보스토크.", getTraveloguePhotoRequests() )); } - public static TravelogueLocationRequest getTravelogueLocationRequest() { - return new TravelogueLocationRequest("37.1234", "127.1234"); + public static TraveloguePositionRequest getTraveloguePositionRequest() { + return new TraveloguePositionRequest("37.1234", "127.1234"); } public static List getTraveloguePhotoRequests() { @@ -101,14 +101,14 @@ public static List getTraveloguePlaceResponses() { .id(1L) .name("블라디보스토크") .description("극동의 진주, 블라디보스토크.") - .location(getTravelogueLocationResponse()) + .position(getTraveloguePositionResponse()) .photoUrls(getTraveloguePhotoUrls()) .build() ); } - public static TravelogueLocationResponse getTravelogueLocationResponse() { - return TravelogueLocationResponse.builder() + public static TraveloguePositionResponse getTraveloguePositionResponse() { + return TraveloguePositionResponse.builder() .lat("37.1234") .lng("127.1234") .build(); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java index 5e24a140..119a93e7 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java @@ -62,8 +62,8 @@ public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, Trav public void initTravelogueTestData() { Travelogue travelogue = persistTravelogue(); TravelogueDay day = persistTravelogueDay(travelogue); - Place location = persistPlace(); - TraveloguePlace place = persistTraveloguePlace(location, day); + Place position = persistPlace(); + TraveloguePlace place = persistTraveloguePlace(position, day); persistTraveloguePhoto(place); } @@ -85,8 +85,8 @@ public Place persistPlace() { return placeRepository.save(place); } - public TraveloguePlace persistTraveloguePlace(Place location, TravelogueDay day) { - TraveloguePlace place = getTraveloguePlace(1, "극동의 진주, 블라디보스토크.", location, day); + public TraveloguePlace persistTraveloguePlace(Place position, TravelogueDay day) { + TraveloguePlace place = getTraveloguePlace(1, "극동의 진주, 블라디보스토크.", position, day); return traveloguePlaceRepository.save(place); } diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java index ec8b9783..3e2f79cd 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -35,8 +35,8 @@ void createPhotos() { List requests = TravelogueTestFixture.getTraveloguePhotoRequests(); Travelogue travelogue = testHelper.persistTravelogue(); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); - Place location = testHelper.persistPlace(); - TraveloguePlace place = testHelper.persistTraveloguePlace(location, day); + Place position = testHelper.persistPlace(); + TraveloguePlace place = testHelper.persistTraveloguePlace(position, day); List photos = photoService.createPhotos(requests, place); @@ -48,8 +48,8 @@ void createPhotos() { void findPhotoUrlsByPlace() { Travelogue travelogue = testHelper.persistTravelogue(); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); - Place location = testHelper.persistPlace(); - TraveloguePlace place = testHelper.persistTraveloguePlace(location, day); + Place position = testHelper.persistPlace(); + TraveloguePlace place = testHelper.persistTraveloguePlace(position, day); TraveloguePhoto photo = testHelper.persistTraveloguePhoto(place); List photoUrls = photoService.findPhotoUrlsByPlace(place); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 8bc88ce4..d6b0d91c 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -68,8 +68,8 @@ void createPlaces() { void findTraveloguePlacesByDay() { Travelogue travelogue = testHelper.persistTravelogue(); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); - Place location = testHelper.persistPlace(); - TraveloguePlace place = testHelper.persistTraveloguePlace(location, day); + Place position = testHelper.persistPlace(); + TraveloguePlace place = testHelper.persistTraveloguePlace(position, day); List places = placeService.findTraveloguePlacesByDay(day); From 1674bb4281637598d5db49130c0127a250e440ea Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:39:30 +0900 Subject: [PATCH 033/108] =?UTF-8?q?[Refactor]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=96=91=EB=B0=A9=ED=96=A5=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: TravelPlan과 관련된 Entity에서 양방향 연관관계 제거 * test: ServiceTest DataJpaTest로 변경 및 TravelPlan 픽스처 분리 * refactor: TravelPlan request/response location -> position 이름 변경 * feat: ServiceTest에 Transactional 전파 끄기 추가 * refactor: 사용하지 않는 TestFixtures 삭제 --- .../touroot/travelplan/domain/TravelPlan.java | 6 +--- .../travelplan/domain/TravelPlanDay.java | 6 +--- .../dto/request/PlanPlaceCreateRequest.java | 4 +-- ...st.java => PlanPositionCreateRequest.java} | 2 +- .../dto/response/TravelPlanPlaceResponse.java | 6 ++-- ...e.java => TravelPlanPositionResponse.java} | 6 ++-- .../repository/TravelPlanDayRepository.java | 5 +++ .../repository/TravelPlanPlaceRepository.java | 5 +++ .../travelplan/service/TravelPlanService.java | 12 ++++--- .../touroot/global/ServiceTest.java | 14 +++++--- .../service/TravelogueDayServiceTest.java | 17 ++++++---- .../service/TravelogueFacadeServiceTest.java | 13 +++++-- .../service/TraveloguePhotoServiceTest.java | 9 +++-- .../service/TraveloguePlaceServiceTest.java | 17 ++++++---- .../service/TravelogueServiceTest.java | 12 ++++--- .../controller/TravelPlanControllerTest.java | 20 +++++------ .../helper/TravelPlanTestHelper.java} | 30 ++++++++++------ .../service/TravelPlanServiceTest.java | 34 ++++++++++--------- 18 files changed, 130 insertions(+), 88 deletions(-) rename backend/src/main/java/woowacourse/touroot/travelplan/dto/request/{PlanLocationCreateRequest.java => PlanPositionCreateRequest.java} (92%) rename backend/src/main/java/woowacourse/touroot/travelplan/dto/response/{TravelPlanLocationResponse.java => TravelPlanPositionResponse.java} (73%) rename backend/src/test/java/woowacourse/touroot/{utils/TestFixture.java => travelplan/helper/TravelPlanTestHelper.java} (69%) diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java index 4699b2bc..b0c9def1 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java @@ -9,7 +9,6 @@ import woowacourse.touroot.global.exception.BadRequestException; import java.time.LocalDate; -import java.util.List; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -27,11 +26,8 @@ public class TravelPlan extends BaseEntity { @Column(nullable = false) private LocalDate startDate; - @OneToMany(mappedBy = "plan") - private List days; - public TravelPlan(String title, LocalDate startDate) { - this(null, title, startDate, null); + this(null, title, startDate); } public void validateStartDate() { diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java index a5c909b0..4f417f14 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java @@ -8,7 +8,6 @@ import woowacourse.touroot.entity.BaseEntity; import java.time.LocalDate; -import java.util.List; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -27,11 +26,8 @@ public class TravelPlanDay extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private TravelPlan plan; - @OneToMany(mappedBy = "day") - private List places; - public TravelPlanDay(int order, TravelPlan plan) { - this(null, order, plan, null); + this(null, order, plan); } public LocalDate getCurrentDate() { diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java index 32f16489..0647fd4e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java @@ -17,7 +17,7 @@ public record PlanPlaceCreateRequest( String description, @Valid @NotNull(message = "위치는 비어있을 수 없습니다.") - PlanLocationCreateRequest location + PlanPositionCreateRequest position ) { public TravelPlanPlace toPlanPlace(int order, TravelPlanDay day, Place place) { @@ -25,6 +25,6 @@ public TravelPlanPlace toPlanPlace(int order, TravelPlanDay day, Place place) { } public Place toPlace() { - return new Place(placeName, location.lat(), location.lng()); + return new Place(placeName, position.lat(), position.lng()); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPositionCreateRequest.java similarity index 92% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPositionCreateRequest.java index ddac4d0f..fd319ac3 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanLocationCreateRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPositionCreateRequest.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -public record PlanLocationCreateRequest( +public record PlanPositionCreateRequest( @Schema(description = "여행 장소 위도", example = "37.5175896") @NotNull(message = "위도는 비어있을 수 없습니다.") String lat, diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java index 118e67cb..e71665e2 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java @@ -8,17 +8,17 @@ @Builder public record TravelPlanPlaceResponse( @Schema(description = "여행 장소 이름") String placeName, - @Schema(description = "여행 장소 위치") TravelPlanLocationResponse location, + @Schema(description = "여행 장소 위치") TravelPlanPositionResponse position, @Schema(description = "여행 장소 설명") String description ) { public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) { Place place = planPlace.getPlace(); - TravelPlanLocationResponse locationResponse = TravelPlanLocationResponse.from(place); + TravelPlanPositionResponse locationResponse = TravelPlanPositionResponse.from(place); return TravelPlanPlaceResponse.builder() .placeName(place.getName()) - .location(locationResponse) + .position(locationResponse) .description(planPlace.getDescription()) .build(); } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPositionResponse.java similarity index 73% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java rename to backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPositionResponse.java index 0824ecb8..d5712ba0 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanLocationResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPositionResponse.java @@ -5,13 +5,13 @@ import woowacourse.touroot.place.domain.Place; @Builder -public record TravelPlanLocationResponse( +public record TravelPlanPositionResponse( @Schema(description = "여행 장소 위도") String lat, @Schema(description = "여행 계획 경도") String lng ) { - public static TravelPlanLocationResponse from(Place place) { - return TravelPlanLocationResponse.builder() + public static TravelPlanPositionResponse from(Place place) { + return TravelPlanPositionResponse.builder() .lat(place.getLatitude()) .lng(place.getLongitude()) .build(); diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java index 18bf271f..4c576c57 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java @@ -1,7 +1,12 @@ package woowacourse.touroot.travelplan.repository; import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelplan.domain.TravelPlan; import woowacourse.touroot.travelplan.domain.TravelPlanDay; +import java.util.List; + public interface TravelPlanDayRepository extends JpaRepository { + + List findByPlan(TravelPlan travelPlan); } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java index 66d04742..d969112b 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java @@ -1,7 +1,12 @@ package woowacourse.touroot.travelplan.repository; import org.springframework.data.jpa.repository.JpaRepository; +import woowacourse.touroot.travelplan.domain.TravelPlanDay; import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +import java.util.List; + public interface TravelPlanPlaceRepository extends JpaRepository { + + List findByDay(TravelPlanDay day); } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java index 714facfa..2027680b 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java @@ -62,8 +62,8 @@ private void createPlanPlace(List request, TravelPlanDay private Place getPlace(PlanPlaceCreateRequest planRequest) { return placeRepository.findByNameAndLatitudeAndLongitude( planRequest.placeName(), - planRequest.location().lat(), - planRequest.location().lng() + planRequest.position().lat(), + planRequest.position().lng() ).orElseGet(() -> placeRepository.save(planRequest.toPlace())); } @@ -79,14 +79,18 @@ private TravelPlan getTravelPlanById(Long planId) { } private List getTravelPlanDayResponses(TravelPlan travelPlan) { - return travelPlan.getDays().stream() + List planDays = travelPlanDayRepository.findByPlan(travelPlan); + + return planDays.stream() .sorted(Comparator.comparing(TravelPlanDay::getOrder)) .map(day -> TravelPlanDayResponse.of(day, getTravelPlanPlaceResponses(day))) .toList(); } private List getTravelPlanPlaceResponses(TravelPlanDay day) { - return day.getPlaces().stream() + List places = travelPlanPlaceRepository.findByDay(day); + + return places.stream() .sorted(Comparator.comparing(TravelPlanPlace::getOrder)) .map(TravelPlanPlaceResponse::from) .toList(); diff --git a/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java b/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java index 954cc446..77d95235 100644 --- a/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java @@ -1,12 +1,18 @@ package woowacourse.touroot.global; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import woowacourse.touroot.utils.DatabaseCleaner; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.TestPropertySource; -@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@DataJpaTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +@Import(value = {DatabaseCleaner.class}) @Retention(RetentionPolicy.RUNTIME) @TestPropertySource(properties = {"spring.config.location = classpath:application-test.yml"}) public @interface ServiceTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java index 0531b5e0..cbbde0ed 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -1,16 +1,10 @@ package woowacourse.touroot.travelogue.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -import java.util.List; -import java.util.Map; 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.context.annotation.Import; import woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelogue.domain.Travelogue; @@ -21,7 +15,16 @@ import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + @DisplayName("여행기 일자 서비스") +@Import(value = {TravelogueDayService.class, TravelogueTestHelper.class}) @ServiceTest class TravelogueDayServiceTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java index a0fb4884..17a0a811 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -1,11 +1,10 @@ package woowacourse.touroot.travelogue.service; -import static org.assertj.core.api.Assertions.assertThat; - 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.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import woowacourse.touroot.global.ServiceTest; @@ -15,7 +14,17 @@ import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; +import static org.assertj.core.api.Assertions.assertThat; + @DisplayName("여행기 Facade 서비스") +@Import(value = { + TravelogueFacadeService.class, + TravelogueService.class, + TraveloguePhotoService.class, + TravelogueDayService.class, + TraveloguePlaceService.class, + TravelogueTestHelper.class, +}) @ServiceTest class TravelogueFacadeServiceTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java index 3e2f79cd..a04555d6 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -1,11 +1,9 @@ package woowacourse.touroot.travelogue.service; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; import woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.travelogue.domain.Travelogue; @@ -16,7 +14,12 @@ import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + @DisplayName("여행기 사진 서비스") +@Import(value = {TraveloguePhotoService.class, TravelogueTestHelper.class}) @ServiceTest class TraveloguePhotoServiceTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java index d6b0d91c..f736dc66 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -1,16 +1,10 @@ package woowacourse.touroot.travelogue.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -import java.util.List; -import java.util.Map; 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.context.annotation.Import; import woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.place.domain.Place; @@ -23,7 +17,16 @@ import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + @DisplayName("여행기 장소 서비스") +@Import(value = {TraveloguePlaceService.class, TravelogueTestHelper.class}) @ServiceTest class TraveloguePlaceServiceTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java index d44e4ded..fba44e00 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java @@ -1,14 +1,10 @@ package woowacourse.touroot.travelogue.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - 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.context.annotation.Import; import org.springframework.data.domain.Pageable; import woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.global.exception.BadRequestException; @@ -18,7 +14,13 @@ import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + @DisplayName("여행기 서비스") +@Import(value = {TravelogueService.class, TravelogueTestHelper.class}) @ServiceTest class TravelogueServiceTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java index f5a417e5..cf177078 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -9,11 +9,11 @@ import org.springframework.boot.test.web.server.LocalServerPort; import woowacourse.touroot.global.AcceptanceTest; import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPositionCreateRequest; import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import woowacourse.touroot.travelplan.helper.TravelPlanTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; -import woowacourse.touroot.utils.TestFixture; import java.time.LocalDate; import java.util.List; @@ -27,12 +27,12 @@ class TravelPlanControllerTest { @LocalServerPort private int port; private final DatabaseCleaner databaseCleaner; - private final TestFixture testFixture; + private final TravelPlanTestHelper testHelper; @Autowired - public TravelPlanControllerTest(DatabaseCleaner databaseCleaner, TestFixture testFixture) { + public TravelPlanControllerTest(DatabaseCleaner databaseCleaner, TravelPlanTestHelper testHelper) { this.databaseCleaner = databaseCleaner; - this.testFixture = testFixture; + this.testHelper = testHelper; } @BeforeEach @@ -45,11 +45,11 @@ void setUp() { @Test void createTravelPlan() { // given - PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .location(locationRequest) + .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() @@ -73,11 +73,11 @@ void createTravelPlan() { @Test void createTravelPlanWithInvalidStartDate() { // given - PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .location(locationRequest) + .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() @@ -101,7 +101,7 @@ void createTravelPlanWithInvalidStartDate() { @Test void readTravelPlan() { // given - testFixture.initTravelPlanTestData(); + testHelper.initTravelPlanTestData(); long id = 1L; // when & then diff --git a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java b/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java similarity index 69% rename from backend/src/test/java/woowacourse/touroot/utils/TestFixture.java rename to backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java index 9567f368..ae60ba23 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/TestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -1,6 +1,5 @@ -package woowacourse.touroot.utils; +package woowacourse.touroot.travelplan.helper; -import java.time.LocalDate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import woowacourse.touroot.place.domain.Place; @@ -12,19 +11,28 @@ import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; import woowacourse.touroot.travelplan.repository.TravelPlanRepository; +import java.time.LocalDate; + @Component -public class TestFixture { +public class TravelPlanTestHelper { - @Autowired - private PlaceRepository placeRepository; + private final PlaceRepository placeRepository; + private final TravelPlanRepository travelPlanRepository; + private final TravelPlanDayRepository travelPlanDayRepository; + private final TravelPlanPlaceRepository travelPlanPlaceRepository; @Autowired - private TravelPlanRepository travelPlanRepository; - @Autowired - private TravelPlanDayRepository travelPlanDayRepository; - @Autowired - private TravelPlanPlaceRepository travelPlanPlaceRepository; - + public TravelPlanTestHelper( + PlaceRepository placeRepository, + TravelPlanRepository travelPlanRepository, + TravelPlanDayRepository travelPlanDayRepository, + TravelPlanPlaceRepository travelPlanPlaceRepository + ) { + this.placeRepository = placeRepository; + this.travelPlanRepository = travelPlanRepository; + this.travelPlanDayRepository = travelPlanDayRepository; + this.travelPlanPlaceRepository = travelPlanPlaceRepository; + } public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { return new Place(name, latitude, longitude, googlePlaceId); diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java index c8a54b05..f4ea0034 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java @@ -1,43 +1,45 @@ package woowacourse.touroot.travelplan.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.time.LocalDate; -import java.util.List; 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.context.annotation.Import; import woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanLocationCreateRequest; import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import woowacourse.touroot.travelplan.dto.request.PlanPositionCreateRequest; import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; +import woowacourse.touroot.travelplan.helper.TravelPlanTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; -import woowacourse.touroot.utils.TestFixture; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("여행 계획 서비스") -// TODO: 양방향 해결 후 @DataJpaTest로 변경 +@Import(value = {TravelPlanService.class, TravelPlanTestHelper.class}) @ServiceTest class TravelPlanServiceTest { private final TravelPlanService travelPlanService; private final DatabaseCleaner databaseCleaner; - private final TestFixture testFixture; + private final TravelPlanTestHelper testHelper; @Autowired public TravelPlanServiceTest( TravelPlanService travelPlanService, DatabaseCleaner databaseCleaner, - TestFixture testFixture + TravelPlanTestHelper testHelper ) { this.travelPlanService = travelPlanService; this.databaseCleaner = databaseCleaner; - this.testFixture = testFixture; + this.testHelper = testHelper; } @BeforeEach @@ -49,11 +51,11 @@ void setUp() { @Test void createTravelPlan() { // given - PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .location(locationRequest) + .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() @@ -73,11 +75,11 @@ void createTravelPlan() { @Test void createTravelPlanWithInvalidStartDate() { // given - PlanLocationCreateRequest locationRequest = new PlanLocationCreateRequest("37.5175896", "127.0867236"); + PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") .description("신나는 여행 장소") - .location(locationRequest) + .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() @@ -97,7 +99,7 @@ void createTravelPlanWithInvalidStartDate() { void readTravelPlan() { // given databaseCleaner.executeTruncate(); - testFixture.initTravelPlanTestData(); + testHelper.initTravelPlanTestData(); Long id = 1L; // when From 7bf447022e12671c540b56e4c210300417f0cc16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:46:07 +0900 Subject: [PATCH 034/108] =?UTF-8?q?[Feature]=20-=20AWS=20S3=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: aws s3 sdk 의존성 추가 * feat: MultipartFile을 S3에 업로드하는 기능 구현 * feat: 이미지를 저장하는 Controller, Service 구현 * style: 불필요한 어노테이션 제거 * feat: 이미지 파일 확장자 검증 로직 추가 * feat: Swagger 기본 코드 추가 * feat: s3 이미지 저장 경로 변경 * feat: Swagger 예외 명세 추가 * feat: 확장자 관련 예외 처리 추가 * style: createFileName 메서드명 createFilePath로 변경 * style: Swagger 어노테이션 분리 * style: 메서드 체이닝 개행 추가 * refactor: MultipartFile을 ImageFile로 포장 및 검증 로직 위치 변경 - AwsS3Provider가 upload의 매개변수로 MultipartFile이 아닌 ImageFile을 받게됨으로써 메서드명도 수정함 --- backend/build.gradle | 4 + .../image/controller/ImageController.java | 41 ++++++++++ .../touroot/image/domain/ImageFile.java | 42 ++++++++++ .../image/infrastructure/AwsS3Provider.java | 78 +++++++++++++++++++ .../touroot/image/service/ImageService.java | 22 ++++++ backend/src/main/resources/application.yml | 5 ++ .../src/test/resources/application-test.yml | 5 ++ 7 files changed, 197 insertions(+) create mode 100644 backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java create mode 100644 backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java create mode 100644 backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java create mode 100644 backend/src/main/java/woowacourse/touroot/image/service/ImageService.java diff --git a/backend/build.gradle b/backend/build.gradle index 83b5f7f5..3ae119d3 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -33,6 +33,10 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'software.amazon.awssdk:s3:2.20.28' + implementation 'software.amazon.awssdk:sts:2.20.28' + implementation 'software.amazon.awssdk:auth:2.20.28' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java b/backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java new file mode 100644 index 00000000..b14c406c --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java @@ -0,0 +1,41 @@ +package woowacourse.touroot.image.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; +import woowacourse.touroot.image.service.ImageService; + +@Tag(name = "이미지") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/image") +public class ImageController { + + private final ImageService imageService; + + @Operation(summary = "이미지 업로드") + @ApiResponses(value = { + @ApiResponse( + responseCode = "400", + description = "jpg, jpeg, png, webp가 아닌 확장자의 파일을 업로드할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PostMapping + public ResponseEntity> uploadImages(@RequestPart List files) { + List imageUrls = imageService.uploadImages(files); + return ResponseEntity.ok(imageUrls); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java b/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java new file mode 100644 index 00000000..68b67965 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java @@ -0,0 +1,42 @@ +package woowacourse.touroot.image.domain; + +import java.util.List; +import lombok.Getter; +import org.springframework.web.multipart.MultipartFile; +import woowacourse.touroot.global.exception.BadRequestException; + +@Getter +public class ImageFile { + + private static final List WHITE_LIST = List.of("jpg", "jpeg", "png", "webp"); + + private final MultipartFile file; + + public ImageFile(MultipartFile file) { + validateImageNames(file); + this.file = file; + } + + private void validateImageNames(MultipartFile file) { + String fileName = file.getOriginalFilename(); + validateNotNull(fileName); + validateExtension(fileName); + } + + private void validateNotNull(String fileName) { + if (fileName == null) { + throw new BadRequestException("파일 형식이 잘못되었습니다."); + } + } + + public void validateExtension(String fileName) { + int extensionIndex = fileName.lastIndexOf("."); + if (extensionIndex == -1 || fileName.endsWith(".")) { + throw new BadRequestException("파일 형식이 잘못되었습니다."); + } + String extension = fileName.substring(extensionIndex + 1); + if (!WHITE_LIST.contains(extension)) { + throw new BadRequestException("파일 형식이 잘못되었습니다."); + } + } +} diff --git a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java new file mode 100644 index 00000000..d24810ba --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java @@ -0,0 +1,78 @@ +package woowacourse.touroot.image.infrastructure; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import woowacourse.touroot.global.exception.BadRequestException; +import woowacourse.touroot.image.domain.ImageFile; + +@Component +public class AwsS3Provider { + + private final String bucket; + private final String directoryPath; + + public AwsS3Provider( + @Value("${cloud.aws.s3.bucket}") String bucket, + @Value("${cloud.aws.s3.directory-path}") String directoryPath + ) { + this.bucket = bucket; + this.directoryPath = directoryPath; + } + + public List uploadImages(List files) { + List urls = new ArrayList<>(); + + try (S3Client s3Client = getS3Client()) { + files.stream() + .map(ImageFile::getFile) + .forEach(file -> { + String filePath = createFilePath(file.getOriginalFilename()); + uploadFile(file, filePath, s3Client); + urls.add(getFileUrl(filePath, s3Client)); + }); + return urls; + } + } + + private S3Client getS3Client() { + return S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(InstanceProfileCredentialsProvider.create()) + .build(); + } + + private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) { + try { + RequestBody requestBody = RequestBody.fromBytes(file.getBytes()); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucket) + .key(createFilePath(filePath)) + .build(); + + s3Client.putObject(putObjectRequest, requestBody); + } catch (IOException e) { + throw new BadRequestException("파일 저장에 실패했습니다."); + } + } + + private String getFileUrl(String filePath, S3Client s3Client) { + return s3Client.utilities() + .getUrl(builder -> builder.bucket(bucket) + .key(filePath)) + .toString(); + } + + private String createFilePath(String fileName) { + return directoryPath + UUID.randomUUID() + fileName.substring(fileName.lastIndexOf(".")); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/image/service/ImageService.java b/backend/src/main/java/woowacourse/touroot/image/service/ImageService.java new file mode 100644 index 00000000..587c5a9e --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/image/service/ImageService.java @@ -0,0 +1,22 @@ +package woowacourse.touroot.image.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import woowacourse.touroot.image.domain.ImageFile; +import woowacourse.touroot.image.infrastructure.AwsS3Provider; + +@RequiredArgsConstructor +@Service +public class ImageService { + + private final AwsS3Provider s3Provider; + + public List uploadImages(List files) { + List imageFiles = files.stream() + .map(ImageFile::new) + .toList(); + return s3Provider.uploadImages(imageFiles); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index bbc7fccd..11873f50 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -7,6 +7,11 @@ jasypt: encryptor: algorithm: PBEWithMD5AndDES iv-generator-classname: org.jasypt.iv.NoIvGenerator +cloud: + aws: + s3: + bucket: techcourse-project-2024 + directory-path: touroot/images/ --- security: jwt: diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index 858d7497..29104de9 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -9,6 +9,11 @@ security: token: secret-key: test-TADG67STFSDAGSDFSG4567UTKJHFHSDFGSR231DF expire-length: 1800000 +cloud: + aws: + s3: + bucket: techcourse-project-2024 + directory-path: touroot/images/ server: port: 8081 spring: From 52d65ee04d9886a96db2e7b3fe7ebff28e3fdf53 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 24 Jul 2024 18:04:24 +0900 Subject: [PATCH 035/108] =?UTF-8?q?[Refactor]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=91=EC=84=B1=20=EC=9A=94=EC=B2=AD=20DTO=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../travelogue/dto/request/TraveloguePhotoRequest.java | 8 ++++---- .../travelogue/dto/request/TraveloguePlaceRequest.java | 2 +- .../travelogue/service/TraveloguePlaceService.java | 2 +- .../travelogue/service/TraveloguePlaceServiceTest.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java index 9e07e8a0..e5c25ac7 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java @@ -6,12 +6,12 @@ import woowacourse.touroot.travelogue.domain.TraveloguePlace; public record TraveloguePhotoRequest( - @Schema(description = "여행기 장소 사진 Key", example = "photo.png") - @NotNull(message = "여행기 장소 사진 Key 값은 비어있을 수 없습니다.") - String key + @Schema(description = "여행기 장소 사진 URL", example = "photo.png") + @NotNull(message = "여행기 장소 사진 URL 값은 비어있을 수 없습니다.") + String url ) { public TraveloguePhoto toTraveloguePhoto(int order, TraveloguePlace traveloguePlace) { - return new TraveloguePhoto(order, key, traveloguePlace); + return new TraveloguePhoto(order, url, traveloguePlace); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index c449974e..dffa4704 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -21,7 +21,7 @@ public record TraveloguePlaceRequest( @Schema(description = "여행기 장소 사진") @NotNull(message = "여행기 장소 사진은 비어있을 수 없습니다.") @Valid - List photos + List photoUrls ) { public TraveloguePlace toTraveloguePlace(int order, Place place, TravelogueDay travelogueDay) { diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java index 441e4844..da2f25ce 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java @@ -32,7 +32,7 @@ public Map> createPlaces( Place place = getPlace(request); TraveloguePlace traveloguePlace = request.toTraveloguePlace(i, place, day); - places.put(traveloguePlaceRepository.save(traveloguePlace), request.photos()); + places.put(traveloguePlaceRepository.save(traveloguePlace), request.photoUrls()); } return places; diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java index f736dc66..9f1e27d6 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -62,7 +62,7 @@ void createPlaces() { assertAll( () -> assertThat(placesMap.keySet()).hasSize(requests.size()), - () -> assertThat(placesMap).containsEntry(places.get(0), requests.get(0).photos()) + () -> assertThat(placesMap).containsEntry(places.get(0), requests.get(0).photoUrls()) ); } From 40ecccb4bd692defc81794ec8bbbe1573f7023fb Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:10:19 +0900 Subject: [PATCH 036/108] =?UTF-8?q?[Feature]=20-=20FE=20develop=20CORS=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/woowacourse/touroot/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java index 5715522c..077cd79a 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java @@ -10,6 +10,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000"); + .allowedOrigins("http://localhost:3000") + .allowedOrigins("https://dev.touroot.kr"); } } From da7fcb629b1bc885ae646e8a39eede641cfbc201 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:48:11 +0900 Subject: [PATCH 037/108] =?UTF-8?q?[Feature]=20-=20FE=20develop=20CORS=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 프론트엔드 dev 서버 cors 추가 * fix: cors 설정 오류 수정 --- .../main/java/woowacourse/touroot/global/config/WebConfig.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java index 077cd79a..e32b6469 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java @@ -10,7 +10,6 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000") - .allowedOrigins("https://dev.touroot.kr"); + .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr"); } } From b1a8747d0f32cabdf80120d88de97d312016e48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:07:17 +0900 Subject: [PATCH 038/108] =?UTF-8?q?[Fix]=20-=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20api=20=EB=B0=98=ED=99=98=20=EA=B0=92=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image/infrastructure/AwsS3Provider.java | 25 ++++++++----------- backend/src/main/resources/application.yml | 1 + .../src/test/resources/application-test.yml | 1 + 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java index d24810ba..d6b33dd7 100644 --- a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java @@ -19,13 +19,16 @@ public class AwsS3Provider { private final String bucket; + private final String imageBaseUri; private final String directoryPath; public AwsS3Provider( @Value("${cloud.aws.s3.bucket}") String bucket, + @Value("${cloud.aws.s3.image-base-uri}") String imageBaseUri, @Value("${cloud.aws.s3.directory-path}") String directoryPath ) { this.bucket = bucket; + this.imageBaseUri = imageBaseUri; this.directoryPath = directoryPath; } @@ -36,14 +39,19 @@ public List uploadImages(List files) { files.stream() .map(ImageFile::getFile) .forEach(file -> { - String filePath = createFilePath(file.getOriginalFilename()); + String newFileName = createNewFileName(file.getOriginalFilename()); + String filePath = directoryPath + newFileName; uploadFile(file, filePath, s3Client); - urls.add(getFileUrl(filePath, s3Client)); + urls.add(imageBaseUri + newFileName); }); return urls; } } + private String createNewFileName(String fileName) { + return UUID.randomUUID() + fileName.substring(fileName.lastIndexOf(".")); + } + private S3Client getS3Client() { return S3Client.builder() .region(Region.AP_NORTHEAST_2) @@ -56,7 +64,7 @@ private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) RequestBody requestBody = RequestBody.fromBytes(file.getBytes()); PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucket) - .key(createFilePath(filePath)) + .key(filePath) .build(); s3Client.putObject(putObjectRequest, requestBody); @@ -64,15 +72,4 @@ private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) throw new BadRequestException("파일 저장에 실패했습니다."); } } - - private String getFileUrl(String filePath, S3Client s3Client) { - return s3Client.utilities() - .getUrl(builder -> builder.bucket(bucket) - .key(filePath)) - .toString(); - } - - private String createFilePath(String fileName) { - return directoryPath + UUID.randomUUID() + fileName.substring(fileName.lastIndexOf(".")); - } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 11873f50..aca1dbe7 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -11,6 +11,7 @@ cloud: aws: s3: bucket: techcourse-project-2024 + image-base-uri: https://dev.touroot.kr/images/ directory-path: touroot/images/ --- security: diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index 29104de9..e77bf260 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -13,6 +13,7 @@ cloud: aws: s3: bucket: techcourse-project-2024 + image-base-uri: https://dev.touroot.kr/images/ directory-path: touroot/images/ server: port: 8081 From d1c4f11f688cde10d1c5a41d10a7ee0c6d754dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:51:34 +0900 Subject: [PATCH 039/108] =?UTF-8?q?[Fix]=20-=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=ED=95=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20url=EC=97=90=20?= =?UTF-8?q?=EC=A0=91=EC=86=8D=ED=95=98=EB=A9=B4=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=97=B4=EC=A7=80=20=EC=95=8A=EA=B3=A0=20?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EB=B0=9B=EB=8A=94=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=ED=95=B4=EA=B2=B0=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../woowacourse/touroot/image/infrastructure/AwsS3Provider.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java index d6b33dd7..336cf86a 100644 --- a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java @@ -65,6 +65,8 @@ private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucket) .key(filePath) + .contentType(file.getContentType()) + .contentLength(file.getSize()) .build(); s3Client.putObject(putObjectRequest, requestBody); From 13428ea8196b549e51faec1fe2333a0a4fcb72d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 25 Jul 2024 01:11:56 +0900 Subject: [PATCH 040/108] =?UTF-8?q?[Refactor]=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0,=20=EC=97=AC=ED=96=89=EA=B3=84=ED=9A=8D=20DTO?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=9A=A9=EC=96=B4=20=ED=86=B5=EC=9D=BC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 여행 계획, 여행기 DTO 도메인 용어 통일 개선 * fix: 수정된 변수명에 따른 테스트 코드 변수명 수정 --- .../travelogue/dto/request/TraveloguePlaceRequest.java | 4 ++-- .../travelogue/dto/response/TraveloguePlaceResponse.java | 4 ++-- .../touroot/travelogue/service/TraveloguePlaceService.java | 2 +- .../touroot/travelogue/fixture/TravelogueTestFixture.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index dffa4704..9ded0184 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -11,7 +11,7 @@ public record TraveloguePlaceRequest( @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") @NotNull(message = "여행기 장소 이름은 비어있을 수 없습니다.") - String name, + String placeName, @Schema(description = "여행기 장소 위치 정보") @NotNull(message = "여행기 장소 위치 정보는 비어있을 수 없습니다.") @Valid @@ -29,6 +29,6 @@ public TraveloguePlace toTraveloguePlace(int order, Place place, TravelogueDay t } public Place toPlace() { - return new Place(name, position.lat(), position.lng()); + return new Place(placeName, position.lat(), position.lng()); } } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java index f80a1757..fc6b92a9 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java @@ -10,7 +10,7 @@ public record TraveloguePlaceResponse( @Schema(description = "여행기 장소 ID", example = "1") Long id, @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") - String name, + String placeName, @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") String description, TraveloguePositionResponse position, @@ -20,7 +20,7 @@ public record TraveloguePlaceResponse( public static TraveloguePlaceResponse of(TraveloguePlace place, List photoUrls) { return TraveloguePlaceResponse.builder() .id(place.getId()) - .name(place.getName()) + .placeName(place.getName()) .description(place.getDescription()) .position(TraveloguePositionResponse.from(place)) .photoUrls(photoUrls) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java index da2f25ce..fa5fb07e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java @@ -40,7 +40,7 @@ public Map> createPlaces( private Place getPlace(TraveloguePlaceRequest request) { return placeRepository.findByNameAndLatitudeAndLongitude( - request.name(), + request.placeName(), request.position().lat(), request.position().lng() ).orElseGet(() -> placeRepository.save(request.toPlace())); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java index 87c0a830..237ed8bf 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java @@ -99,7 +99,7 @@ public static List getTravelogueDayResponses() { public static List getTraveloguePlaceResponses() { return List.of(TraveloguePlaceResponse.builder() .id(1L) - .name("블라디보스토크") + .placeName("블라디보스토크") .description("극동의 진주, 블라디보스토크.") .position(getTraveloguePositionResponse()) .photoUrls(getTraveloguePhotoUrls()) From dba635dec06eff648914489a1f9a0c43c539802c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 25 Jul 2024 03:36:41 +0900 Subject: [PATCH 041/108] =?UTF-8?q?[Fix]=20Kakao=20Redirect=20Uri=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=9E=9C=EB=94=A9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index aca1dbe7..7ab73e73 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -41,7 +41,7 @@ spring: defer-datasource-initialization: true oauth: kakao: - redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao + redirect-uri: http://localhost:3000/oauth/kakao/callback --- security: jwt: @@ -75,4 +75,4 @@ spring: ddl-auto: none oauth: kakao: - redirect-uri: http://api-dev.touroot.kr/api/v1/login/oauth/kakao + redirect-uri: http://localhost:3000/oauth/kakao/callback From c8616cb5fce1561be64cd28393e4104f1a6df0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 25 Jul 2024 03:58:54 +0900 Subject: [PATCH 042/108] =?UTF-8?q?[Fix]=20Kakao=20Oauth=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20api=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=88=98=EC=A0=95=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: kakao login redirect uri 로그인 랜딩 페이지로 수정 * fix: login api HTTP METHOD POST로 변경 --- .../touroot/authentication/controller/LoginController.java | 4 ++-- backend/src/main/resources/application.yml | 4 ++-- backend/src/test/resources/application-test.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java index abb762b8..36cb6aee 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -33,7 +33,7 @@ public class LoginController { ) } ) - @GetMapping("/oauth/kakao") + @PostMapping("/oauth/kakao") public ResponseEntity login(@RequestParam(name = "code") String authorizationCode) { return ResponseEntity.ok() .body(loginService.login(authorizationCode)); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 7ab73e73..698fc8e3 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -41,7 +41,7 @@ spring: defer-datasource-initialization: true oauth: kakao: - redirect-uri: http://localhost:3000/oauth/kakao/callback + redirect-uri: http://localhost:3000/oauth --- security: jwt: @@ -75,4 +75,4 @@ spring: ddl-auto: none oauth: kakao: - redirect-uri: http://localhost:3000/oauth/kakao/callback + redirect-uri: http://localhost:3000/oauth diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index e77bf260..bff10f5c 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -2,7 +2,7 @@ oauth: kakao: user-information-request-uri: https://kapi.kakao.com/v2/user/me access-token-request-uri: https://kauth.kakao.com/oauth/token - redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao + redirect-uri: http://localhost:3000/oauth rest-api-key: test-api-key security: jwt: From c4c0596521f9e5f93e9e118a68479d404e02894f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 25 Jul 2024 11:10:07 +0900 Subject: [PATCH 043/108] =?UTF-8?q?[Refactor]=20-=20Oauth=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B3=BC=EC=A0=95=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: example과 response 추가를 통한 api 명세 구체화 개선 * refactor: JwtTokenProvider에서 도메인 의존 제거 개선 * test: JwtTokenProvider 토큰 생성 테스트 작성 * refactor: dto 패키지 계층 및 이너 클래스 구조 분리 * test: 테스트 클래스 단위 DisplayName 설정 * refactor: Member All Argument 생성자 접근 제어 수준 완화 개선 * test: LoginService Mock Test 작성 * test: LoginController 클래스 슬라이스 테스트 작성 * fix: api 명세 수정 * style: 공백 컨벤션에 맞게 수정 개선 --- .../controller/LoginController.java | 25 ++++--- .../dto/OauthUserInformationResponse.java | 30 -------- .../dto/{ => response}/LoginResponse.java | 8 +-- .../OauthUserInformationResponse.java | 21 ++++++ .../kakao}/KakaoAccessTokenResponse.java | 2 +- .../dto/response/kakao/KakaoAccount.java | 6 ++ .../dto/response/kakao/KakaoProfile.java | 9 +++ .../infrastructure/JwtTokenProvider.java | 7 +- .../infrastructure/KakaoOauthClient.java | 4 +- .../infrastructure/KakaoOauthProvider.java | 2 +- .../authentication/service/LoginService.java | 8 +-- .../touroot/member/domain/Member.java | 2 +- .../controller/LoginControllerTest.java | 45 ++++++++++++ .../authentication/fixture/MemberFixture.java | 8 +++ .../fixture/OauthUserInformationFixture.java | 12 ++++ .../infrastructure/JwtTokenProviderTest.java | 25 +++++++ .../service/LoginServiceTest.java | 71 +++++++++++++++++++ 17 files changed, 227 insertions(+), 58 deletions(-) delete mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java rename backend/src/main/java/woowacourse/touroot/authentication/dto/{ => response}/LoginResponse.java (60%) create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java rename backend/src/main/java/woowacourse/touroot/authentication/dto/{ => response/kakao}/KakaoAccessTokenResponse.java (88%) create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java create mode 100644 backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java create mode 100644 backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java create mode 100644 backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java diff --git a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java index 36cb6aee..e3aee7b5 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -11,7 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import woowacourse.touroot.authentication.dto.LoginResponse; +import woowacourse.touroot.authentication.dto.response.LoginResponse; import woowacourse.touroot.authentication.service.LoginService; import woowacourse.touroot.global.exception.dto.ExceptionResponse; @@ -23,16 +24,18 @@ public class LoginController { private final LoginService loginService; - @Operation( - summary = "카카오 로그인", - responses = { - @ApiResponse( - responseCode = "400", - description = "유효하지 않은 인가 코드로 로그인 요청을 했을 때", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - } - ) + @Operation(summary = "카카오 소셜 로그인") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 파라미터에 올바르지 않은 인가코드값이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) @PostMapping("/oauth/kakao") public ResponseEntity login(@RequestParam(name = "code") String authorizationCode) { return ResponseEntity.ok() diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java deleted file mode 100644 index 3a453147..00000000 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/OauthUserInformationResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package woowacourse.touroot.authentication.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record OauthUserInformationResponse( - @JsonProperty("id") - Long socialLoginId, - @JsonProperty("kakao_account") - KakaoAccount kakaoAccount -) { - - public String nickname() { - return kakaoAccount.kakaoProfile.nickname; - } - - public String profileImage() { - return kakaoAccount.kakaoProfile.image; - } - - private record KakaoAccount( - @JsonProperty("profile") KakaoProfile kakaoProfile - ) { - } - - private record KakaoProfile( - @JsonProperty("nickname") String nickname, - @JsonProperty("profile_image_url") String image - ) { - } -} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/LoginResponse.java similarity index 60% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java rename to backend/src/main/java/woowacourse/touroot/authentication/dto/response/LoginResponse.java index a4cccb33..f5abc640 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/LoginResponse.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/LoginResponse.java @@ -1,12 +1,12 @@ -package woowacourse.touroot.authentication.dto; +package woowacourse.touroot.authentication.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import woowacourse.touroot.member.domain.Member; public record LoginResponse( - @Schema(description = "로그인된 유저의 닉네임") String nickname, - @Schema(description = "로그인된 유저의 프로필 이미지 경로") String profileImageUrl, - @Schema(description = "인가에 필요한 accessToken") String accessToken) { + @Schema(description = "로그인된 유저의 닉네임", example = "리비") String nickname, + @Schema(description = "로그인된 유저의 프로필 이미지 경로", example = "https://img-ul") String profileImageUrl, + @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") String accessToken) { public static LoginResponse of(Member member, String accessToken) { return new LoginResponse(member.getNickname(), member.getProfileImageUri(), accessToken); diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java new file mode 100644 index 00000000..5d797078 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java @@ -0,0 +1,21 @@ +package woowacourse.touroot.authentication.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import woowacourse.touroot.authentication.dto.response.kakao.KakaoAccount; + +public record OauthUserInformationResponse( + @JsonProperty("id") + Long socialLoginId, + @JsonProperty("kakao_account") + KakaoAccount kakaoAccount +) { + + public String nickname() { + return kakaoAccount.kakaoProfile().nickname(); + } + + public String profileImage() { + return kakaoAccount.kakaoProfile().image(); + } + +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java similarity index 88% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java rename to backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java index 47f5ec11..d6a8f986 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/KakaoAccessTokenResponse.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.dto; +package woowacourse.touroot.authentication.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java new file mode 100644 index 00000000..b025be37 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java @@ -0,0 +1,6 @@ +package woowacourse.touroot.authentication.dto.response.kakao; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoAccount(@JsonProperty("profile") KakaoProfile kakaoProfile) { +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java new file mode 100644 index 00000000..7451c4a0 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java @@ -0,0 +1,9 @@ +package woowacourse.touroot.authentication.dto.response.kakao; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record KakaoProfile( + @JsonProperty("nickname") String nickname, + @JsonProperty("profile_image_url") String image +) { +} diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java index 1b8f3ee9..383d1ce6 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java @@ -5,7 +5,6 @@ import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import woowacourse.touroot.member.domain.Member; @Component public class JwtTokenProvider { @@ -23,13 +22,13 @@ public JwtTokenProvider( this.validityInMilliseconds = validityInMilliseconds; } - public String createToken(Member member) { + public String createToken(Long memberId) { Date now = new Date(); Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() - .setSubject(member.getId().toString()) - .claim(MEMBER_ID_KEY, member.getId()) + .setSubject(memberId.toString()) + .claim(MEMBER_ID_KEY, memberId) .setExpiration(validity) .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) .compact(); diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java index 461bea3b..bf76c598 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java @@ -15,8 +15,8 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; -import woowacourse.touroot.authentication.dto.KakaoAccessTokenResponse; -import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; +import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; +import woowacourse.touroot.authentication.dto.response.kakao.KakaoAccessTokenResponse; import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.global.exception.ClientException; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java index d23b7de7..e0c75901 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; +import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; @RequiredArgsConstructor @Component diff --git a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java b/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java index abb1a04a..a380b6ee 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java @@ -2,8 +2,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import woowacourse.touroot.authentication.dto.LoginResponse; -import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; +import woowacourse.touroot.authentication.dto.response.LoginResponse; +import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; import woowacourse.touroot.authentication.infrastructure.KakaoOauthProvider; import woowacourse.touroot.member.domain.Member; @@ -22,9 +22,9 @@ public LoginResponse login(String code) { Member member = memberRepository.findByKakaoId(userInformation.socialLoginId()) .orElseGet(() -> signUp(userInformation)); - return LoginResponse.of(member, tokenProvider.createToken(member)); + return LoginResponse.of(member, tokenProvider.createToken(member.getId())); } - + private Member signUp(OauthUserInformationResponse userInformation) { return memberRepository.save( new Member(userInformation.socialLoginId(), userInformation.nickname(), userInformation.profileImage()) diff --git a/backend/src/main/java/woowacourse/touroot/member/domain/Member.java b/backend/src/main/java/woowacourse/touroot/member/domain/Member.java index 898f5e87..3aac33ec 100644 --- a/backend/src/main/java/woowacourse/touroot/member/domain/Member.java +++ b/backend/src/main/java/woowacourse/touroot/member/domain/Member.java @@ -12,7 +12,7 @@ import woowacourse.touroot.entity.BaseEntity; @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PUBLIC) @Getter @Entity public class Member extends BaseEntity { diff --git a/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java new file mode 100644 index 00000000..bc34d81a --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java @@ -0,0 +1,45 @@ +package woowacourse.touroot.authentication.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import woowacourse.touroot.authentication.dto.response.LoginResponse; +import woowacourse.touroot.authentication.service.LoginService; + +@WebMvcTest(LoginController.class) +class LoginControllerTest { + + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockBean + private LoginService loginService; + @MockBean + JpaMetamodelMappingContext jpaMetamodelMappingContext; + + @DisplayName("카카오 로그인 요청을 처리할 수 있다") + @Test + void loginTest() throws Exception { + LoginResponse loginResponse = new LoginResponse("리비", "img-url", "test-access-token"); + when(loginService.login(any(String.class))).thenReturn(loginResponse); + + mockMvc.perform(post("/api/v1/login/oauth/kakao") + .param("code", "test-authorization-code")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(objectMapper.writeValueAsString(loginResponse))); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java b/backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java new file mode 100644 index 00000000..0a4e1778 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java @@ -0,0 +1,8 @@ +package woowacourse.touroot.authentication.fixture; + +import woowacourse.touroot.member.domain.Member; + +public class MemberFixture { + + public static final Member MEMBER_1 = new Member(1L, 1L, "리비", "image-url"); +} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java b/backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java new file mode 100644 index 00000000..bc4f034b --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java @@ -0,0 +1,12 @@ +package woowacourse.touroot.authentication.fixture; + +import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; +import woowacourse.touroot.authentication.dto.response.kakao.KakaoAccount; +import woowacourse.touroot.authentication.dto.response.kakao.KakaoProfile; + +public class OauthUserInformationFixture { + + public static final OauthUserInformationResponse USER_1_OAUTH_INFORMATION = new OauthUserInformationResponse( + 1L, new KakaoAccount(new KakaoProfile("리비", "img-url")) + ); +} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java b/backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java new file mode 100644 index 00000000..530f0fb7 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java @@ -0,0 +1,25 @@ +package woowacourse.touroot.authentication.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("JWT") +class JwtTokenProviderTest { + + private static final String TEST_SECRET_KEY = "test-secret-key-clover-leegun-naknak-alpaka-libi"; + private static final int EXPIRATION_TIME_30_MIN = 1800000; + private static final int EXPIRATION_TIME_0 = 0; + + private JwtTokenProvider jwtTokenProvider; + + @DisplayName("멤버를 통해 토큰을 만들 수 있다") + @Test + void createTokenTest() { + jwtTokenProvider = new JwtTokenProvider(TEST_SECRET_KEY, EXPIRATION_TIME_30_MIN); + + assertThat(jwtTokenProvider.createToken(1L)) + .isNotNull(); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java b/backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java new file mode 100644 index 00000000..397a1549 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java @@ -0,0 +1,71 @@ +package woowacourse.touroot.authentication.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import woowacourse.touroot.authentication.dto.response.LoginResponse; +import woowacourse.touroot.authentication.fixture.MemberFixture; +import woowacourse.touroot.authentication.fixture.OauthUserInformationFixture; +import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; +import woowacourse.touroot.authentication.infrastructure.KakaoOauthProvider; +import woowacourse.touroot.member.domain.Member; +import woowacourse.touroot.member.repository.MemberRepository; + +@DisplayName("로그인 서비스") +@ExtendWith(MockitoExtension.class) +class LoginServiceTest { + + private static final String AUTHENTICATION_CODE = "test-authentication-code"; + + @InjectMocks + private LoginService loginService; + @Mock + private MemberRepository memberRepository; + @Mock + private KakaoOauthProvider kakaoOauthProvider; + @Mock + JwtTokenProvider jwtTokenProvider; + + @DisplayName("투룻 회원가입이 되어 있는 회원의 카카오 소셜 로그인을 처리할 수 있다") + @Test + void existUserKakaoSocialLoginTest() { + // given + when(kakaoOauthProvider.getUserInformation(AUTHENTICATION_CODE)) + .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); + when(memberRepository.findByKakaoId(any(Long.class))) + .thenReturn(Optional.of(MemberFixture.MEMBER_1)); + LoginResponse response = loginService.login(AUTHENTICATION_CODE); + + // when & then + assertThat(response).isEqualTo( + LoginResponse.of(MemberFixture.MEMBER_1, response.accessToken())); + } + + @DisplayName("투룻 회원가입이 되어 있지 않은 회원은 소셜 로그인 과정에서 회원가입 후 로그인 된다") + @Test + void nonExistUserKakaoSocialLoginTest() { + // given + when(kakaoOauthProvider.getUserInformation(AUTHENTICATION_CODE)) + .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); + when(memberRepository.findByKakaoId(any(Long.class))) + .thenReturn(Optional.empty()); + when(memberRepository.save(any(Member.class))) + .thenReturn(MemberFixture.MEMBER_1); + LoginResponse response = loginService.login(AUTHENTICATION_CODE); + + // when & then + assertThat(response).isEqualTo( + LoginResponse.of(MemberFixture.MEMBER_1, response.accessToken())); + verify(memberRepository, times(1)).save(any(Member.class)); + } +} From 998f96bcbdb569776bee3555b015b834deb181bf Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:27:52 +0900 Subject: [PATCH 044/108] =?UTF-8?q?[Feature]=20-=20Github=20action=20gradl?= =?UTF-8?q?e=20=EC=BA=90=EC=8B=B1=20=EB=8F=84=EC=9E=85=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/be-cd-dev.yml | 10 ++++++++++ .github/workflows/be-ci.yml | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/be-cd-dev.yml b/.github/workflows/be-cd-dev.yml index 4bc39d4a..20ea7396 100644 --- a/.github/workflows/be-cd-dev.yml +++ b/.github/workflows/be-cd-dev.yml @@ -24,6 +24,16 @@ jobs: - name: Make keystore file run: echo "${{secrets.SSL_KEYSTORE}}" | base64 --decode > ./src/main/resources/keystore.p12 + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{runner.os}}-gradle-${{hashFiles('**/*.gradle*', '**/gradle-wrapper.properties')}} + restore-keys: | + ${{runner.os}}-gradle- + - name: Setup Gradle uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 diff --git a/.github/workflows/be-ci.yml b/.github/workflows/be-ci.yml index 648c43e3..106307c3 100644 --- a/.github/workflows/be-ci.yml +++ b/.github/workflows/be-ci.yml @@ -24,6 +24,16 @@ jobs: - name: Make keystore file run: echo "${{secrets.SSL_KEYSTORE}}" | base64 --decode > ./src/main/resources/keystore.p12 + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{runner.os}}-gradle-${{hashFiles('**/*.gradle*', '**/gradle-wrapper.properties')}} + restore-keys: | + ${{runner.os}}-gradle- + - name: Grant execute permission for gradlew run: chmod +x gradlew From 6389b30fd945b175fe5f77bd8b4e676d7555caee Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Thu, 25 Jul 2024 15:57:45 +0900 Subject: [PATCH 045/108] =?UTF-8?q?[Refactor]=20-=20Test=20fixture?= =?UTF-8?q?=EB=A5=BC=20Enum=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Test fixture를 도메인 별로 분리하고 enum화 * chore: 사용하지 않는 import 구문 제거 --- .../touroot/place/fixture/PlaceFixture.java | 24 ++++ .../controller/TravelogueControllerTest.java | 9 +- .../fixture/TravelogueDayFixture.java | 27 ++++ .../travelogue/fixture/TravelogueFixture.java | 20 +++ .../fixture/TraveloguePhotoFixture.java | 29 +++++ .../fixture/TraveloguePlaceFixture.java | 33 +++++ .../fixture/TravelogueRequestFixture.java | 41 ++++++ .../fixture/TravelogueResponseFixture.java | 65 ++++++++++ .../fixture/TravelogueTestFixture.java | 120 ------------------ .../helper/TravelogueTestHelper.java | 38 ++---- .../service/TravelogueDayServiceTest.java | 4 +- .../service/TravelogueFacadeServiceTest.java | 11 +- .../service/TraveloguePhotoServiceTest.java | 4 +- .../service/TraveloguePlaceServiceTest.java | 4 +- .../service/TravelogueServiceTest.java | 6 +- 15 files changed, 270 insertions(+), 165 deletions(-) create mode 100644 backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java create mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java delete mode 100644 backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java diff --git a/backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java b/backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java new file mode 100644 index 00000000..2cf69db5 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java @@ -0,0 +1,24 @@ +package woowacourse.touroot.place.fixture; + +import woowacourse.touroot.place.domain.Place; + +public enum PlaceFixture { + + PLACE("함덕해수욕장", "34.54343", "126.66977", ""); + + private final String name; + private final String latitude; + private final String longitude; + private final String googlePlaceId; + + PlaceFixture(String name, String latitude, String longitude, String googlePlaceId) { + this.name = name; + this.latitude = latitude; + this.longitude = longitude; + this.googlePlaceId = googlePlaceId; + } + + public Place get() { + return new Place(name, latitude, longitude, googlePlaceId); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java index 32132ca5..bcfba99b 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java @@ -15,7 +15,8 @@ import woowacourse.touroot.global.AcceptanceTest; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueResponseFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; @@ -50,7 +51,7 @@ void setUp() { @DisplayName("여행기를 작성한다.") @Test void createTravelogue() { - TravelogueRequest request = TravelogueTestFixture.getTravelogueRequest(); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -65,7 +66,7 @@ void createTravelogue() { @Test void findTravelogue() throws JsonProcessingException { testHelper.initTravelogueTestData(); - TravelogueResponse response = TravelogueTestFixture.getTravelogueResponse(); + TravelogueResponse response = TravelogueResponseFixture.getTravelogueResponse(); RestAssured.given().log().all() .accept(ContentType.JSON) @@ -79,7 +80,7 @@ void findTravelogue() throws JsonProcessingException { @Test void findMainPageTravelogues() throws JsonProcessingException { testHelper.initTravelogueTestData(); - Page responses = TravelogueTestFixture.getTravelogueResponses(); + Page responses = TravelogueResponseFixture.getTravelogueResponses(); RestAssured.given().log().all() .accept(ContentType.JSON) diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java new file mode 100644 index 00000000..97449d60 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java @@ -0,0 +1,27 @@ +package woowacourse.touroot.travelogue.fixture; + +import static woowacourse.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; + +import woowacourse.touroot.travelogue.domain.Travelogue; +import woowacourse.touroot.travelogue.domain.TravelogueDay; + +public enum TravelogueDayFixture { + + TRAVELOGUE_DAY(1, TRAVELOGUE.get()); + + private final int order; + private final Travelogue travelogue; + + TravelogueDayFixture(int order, Travelogue travelogue) { + this.order = order; + this.travelogue = travelogue; + } + + public TravelogueDay get() { + return new TravelogueDay(order, travelogue); + } + + public TravelogueDay create(int order, Travelogue travelogue) { + return new TravelogueDay(order, travelogue); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java new file mode 100644 index 00000000..b5ad9e8e --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java @@ -0,0 +1,20 @@ +package woowacourse.touroot.travelogue.fixture; + +import woowacourse.touroot.travelogue.domain.Travelogue; + +public enum TravelogueFixture { + + TRAVELOGUE("제주에 하영 옵서", "https://url.com/jeju_thumbnail.png"); + + private final String title; + private final String thumbnail; + + TravelogueFixture(String title, String thumbnail) { + this.title = title; + this.thumbnail = thumbnail; + } + + public Travelogue get() { + return new Travelogue(title, thumbnail); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java new file mode 100644 index 00000000..0667011a --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java @@ -0,0 +1,29 @@ +package woowacourse.touroot.travelogue.fixture; + +import static woowacourse.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; + +import woowacourse.touroot.travelogue.domain.TraveloguePhoto; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; + +public enum TraveloguePhotoFixture { + + TRAVELOGUE_PHOTO(1, "https://image-url.com/image1.png", TRAVELOGUE_PLACE.get()); + + private final int order; + private final String url; + private final TraveloguePlace place; + + TraveloguePhotoFixture(int order, String url, TraveloguePlace place) { + this.order = order; + this.url = url; + this.place = place; + } + + public TraveloguePhoto get() { + return new TraveloguePhoto(order, url, place); + } + + public TraveloguePhoto create(TraveloguePlace place) { + return new TraveloguePhoto(order, url, place); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java new file mode 100644 index 00000000..c63a28f0 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java @@ -0,0 +1,33 @@ +package woowacourse.touroot.travelogue.fixture; + +import static woowacourse.touroot.place.fixture.PlaceFixture.PLACE; +import static woowacourse.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; + +import woowacourse.touroot.place.domain.Place; +import woowacourse.touroot.travelogue.domain.TravelogueDay; +import woowacourse.touroot.travelogue.domain.TraveloguePlace; + +public enum TraveloguePlaceFixture { + + TRAVELOGUE_PLACE(1, "에메랄드 빛 해변", PLACE.get(), TRAVELOGUE_DAY.get()); + + private final int order; + private final String description; + private final Place place; + private final TravelogueDay day; + + TraveloguePlaceFixture(int order, String description, Place place, TravelogueDay day) { + this.order = order; + this.description = description; + this.place = place; + this.day = day; + } + + public TraveloguePlace get() { + return new TraveloguePlace(order, description, place, day); + } + + public TraveloguePlace create(Place place, TravelogueDay day) { + return new TraveloguePlace(order, description, place, day); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java new file mode 100644 index 00000000..c5f64778 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -0,0 +1,41 @@ +package woowacourse.touroot.travelogue.fixture; + +import java.util.List; +import org.springframework.stereotype.Component; +import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import woowacourse.touroot.travelogue.dto.request.TraveloguePositionRequest; +import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; + +@Component +public class TravelogueRequestFixture { + + private TravelogueRequestFixture() { + } + + public static TravelogueRequest getTravelogueRequest() { + return new TravelogueRequest("제주에 하영 옵서", "https://url.com/jeju_thumbnail.png", getTravelogueDayRequests()); + } + + public static List getTravelogueDayRequests() { + return List.of(new TravelogueDayRequest(getTraveloguePlaceRequests())); + } + + public static List getTraveloguePlaceRequests() { + return List.of(new TraveloguePlaceRequest( + "함덕해수욕장", + getTraveloguePositionRequest(), + "에메랄드 빛 해변", + getTraveloguePhotoRequests() + )); + } + + public static TraveloguePositionRequest getTraveloguePositionRequest() { + return new TraveloguePositionRequest("34.54343", "126.66977"); + } + + public static List getTraveloguePhotoRequests() { + return List.of(new TraveloguePhotoRequest("https://image-url.com/image1.png")); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java new file mode 100644 index 00000000..f8a36b23 --- /dev/null +++ b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -0,0 +1,65 @@ +package woowacourse.touroot.travelogue.fixture; + +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.stereotype.Component; +import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; +import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import woowacourse.touroot.travelogue.dto.response.TraveloguePositionResponse; +import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; + +@Component +public class TravelogueResponseFixture { + + private TravelogueResponseFixture() { + } + + public static TravelogueResponse getTravelogueResponse() { + return TravelogueResponse.builder() + .id(1L) + .title("제주에 하영 옵서") + .thumbnail("https://url.com/jeju_thumbnail.png") + .days(getTravelogueDayResponses()) + .build(); + } + + public static Page getTravelogueResponses() { + return new PageImpl<>(List.of(TravelogueResponse.builder() + .id(1L) + .title("제주에 하영 옵서") + .thumbnail("https://url.com/jeju_thumbnail.png") + .days(getTravelogueDayResponses()) + .build())); + } + + public static List getTravelogueDayResponses() { + return List.of(TravelogueDayResponse.builder() + .id(1L) + .places(getTraveloguePlaceResponses()) + .build() + ); + } + + public static List getTraveloguePlaceResponses() { + return List.of(TraveloguePlaceResponse.builder() + .id(1L) + .placeName("함덕해수욕장") + .description("에메랄드 빛 해변") + .position(getTraveloguePositionResponse()) + .photoUrls(getTraveloguePhotoUrls()) + .build() + ); + } + + public static TraveloguePositionResponse getTraveloguePositionResponse() { + return TraveloguePositionResponse.builder() + .lat("34.54343") + .lng("126.66977") + .build(); + } + + public static List getTraveloguePhotoUrls() { + return List.of("https://image-url.com/image1.png"); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java b/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java deleted file mode 100644 index 237ed8bf..00000000 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueTestFixture.java +++ /dev/null @@ -1,120 +0,0 @@ -package woowacourse.touroot.travelogue.fixture; - -import java.util.List; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.stereotype.Component; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePositionRequest; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; -import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; -import woowacourse.touroot.travelogue.dto.response.TraveloguePositionResponse; -import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; - -@Component -public class TravelogueTestFixture { - - private TravelogueTestFixture() { - } - - public static Travelogue getTravelogue() { - return new Travelogue("낭만의 시베리아 횡단철도 여행", "https://photo-key.jpeg"); - } - - public static TravelogueDay getTravelogueDay() { - return new TravelogueDay(1, getTravelogue()); - } - - public static Place getPlace() { - return new Place("블라디보스토크", "37.1234", "127.1234", ""); - } - - public static TraveloguePlace getTraveloguePlace() { - return new TraveloguePlace(1, "극동의 진주, 블라디보스토크.", getPlace(), getTravelogueDay()); - } - - public static TraveloguePhoto getTraveloguePhoto() { - return new TraveloguePhoto(1, "https://photo-key.jpeg", getTraveloguePlace()); - } - - public static TravelogueRequest getTravelogueRequest() { - return new TravelogueRequest("낭만의 시베리아 횡단철도 여행", "https://photo-key.jpeg", getTravelogueDayRequests()); - } - - public static List getTravelogueDayRequests() { - return List.of(new TravelogueDayRequest(getTraveloguePlaceRequests())); - } - - public static List getTraveloguePlaceRequests() { - return List.of(new TraveloguePlaceRequest( - "블라디보스토크", - getTraveloguePositionRequest(), - "극동의 진주, 블라디보스토크.", - getTraveloguePhotoRequests() - )); - } - - public static TraveloguePositionRequest getTraveloguePositionRequest() { - return new TraveloguePositionRequest("37.1234", "127.1234"); - } - - public static List getTraveloguePhotoRequests() { - return List.of(new TraveloguePhotoRequest("https://photo-key.jpeg")); - } - - public static TravelogueResponse getTravelogueResponse() { - return TravelogueResponse.builder() - .id(1L) - .title("낭만의 시베리아 횡단철도 여행") - .thumbnail("https://photo-key.jpeg") - .days(getTravelogueDayResponses()) - .build(); - } - - public static Page getTravelogueResponses() { - return new PageImpl<>(List.of(TravelogueResponse.builder() - .id(1L) - .title("낭만의 시베리아 횡단철도 여행") - .thumbnail("https://photo-key.jpeg") - .days(getTravelogueDayResponses()) - .build())); - } - - public static List getTravelogueDayResponses() { - return List.of(TravelogueDayResponse.builder() - .id(1L) - .places(getTraveloguePlaceResponses()) - .build() - ); - } - - public static List getTraveloguePlaceResponses() { - return List.of(TraveloguePlaceResponse.builder() - .id(1L) - .placeName("블라디보스토크") - .description("극동의 진주, 블라디보스토크.") - .position(getTraveloguePositionResponse()) - .photoUrls(getTraveloguePhotoUrls()) - .build() - ); - } - - public static TraveloguePositionResponse getTraveloguePositionResponse() { - return TraveloguePositionResponse.builder() - .lat("37.1234") - .lng("127.1234") - .build(); - } - - public static List getTraveloguePhotoUrls() { - return List.of("https://photo-key.jpeg"); - } -} diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java index 119a93e7..26fa6d3a 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,5 +1,11 @@ package woowacourse.touroot.travelogue.helper; +import static woowacourse.touroot.place.fixture.PlaceFixture.PLACE; +import static woowacourse.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; +import static woowacourse.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; +import static woowacourse.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; +import static woowacourse.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import woowacourse.touroot.place.domain.Place; @@ -8,7 +14,6 @@ import woowacourse.touroot.travelogue.domain.TravelogueDay; import woowacourse.touroot.travelogue.domain.TraveloguePhoto; import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; @@ -38,27 +43,6 @@ public TravelogueTestHelper( this.traveloguePhotoRepository = traveloguePhotoRepository; } - public static Travelogue getTravelogue(String name, String thumbnail) { - return new Travelogue(name, thumbnail); - } - - public static TravelogueDay getTravelogueDay(Integer order, Travelogue travelogue) { - return new TravelogueDay(order, travelogue); - } - - public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { - return new Place(name, latitude, longitude, googlePlaceId); - } - - public static TraveloguePlace getTraveloguePlace(Integer order, String description, Place place, - TravelogueDay travelogueDay) { - return new TraveloguePlace(order, description, place, travelogueDay); - } - - public static TraveloguePhoto getTraveloguePhoto(String key, Integer order, TraveloguePlace traveloguePlace) { - return new TraveloguePhoto(order, key, traveloguePlace); - } - public void initTravelogueTestData() { Travelogue travelogue = persistTravelogue(); TravelogueDay day = persistTravelogueDay(travelogue); @@ -68,31 +52,31 @@ public void initTravelogueTestData() { } public Travelogue persistTravelogue() { - Travelogue travelogue = TravelogueTestFixture.getTravelogue(); + Travelogue travelogue = TRAVELOGUE.get(); return travelogueRepository.save(travelogue); } public TravelogueDay persistTravelogueDay(Travelogue travelogue) { - TravelogueDay day = TravelogueTestHelper.getTravelogueDay(1, travelogue); + TravelogueDay day = TRAVELOGUE_DAY.create(1, travelogue); return travelogueDayRepository.save(day); } public Place persistPlace() { - Place place = TravelogueTestFixture.getPlace(); + Place place = PLACE.get(); return placeRepository.save(place); } public TraveloguePlace persistTraveloguePlace(Place position, TravelogueDay day) { - TraveloguePlace place = getTraveloguePlace(1, "극동의 진주, 블라디보스토크.", position, day); + TraveloguePlace place = TRAVELOGUE_PLACE.create(position, day); return traveloguePlaceRepository.save(place); } public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { - TraveloguePhoto photo = getTraveloguePhoto("https://photo-key.jpeg", 1, place); + TraveloguePhoto photo = TRAVELOGUE_PHOTO.create(place); return traveloguePhotoRepository.save(photo); } diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java index cbbde0ed..3dfa5e81 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -11,7 +11,7 @@ import woowacourse.touroot.travelogue.domain.TravelogueDay; import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; @@ -51,7 +51,7 @@ void setUp() { @DisplayName("여행기의 일자들을 생성한다.") @Test void createDays() { - List requests = TravelogueTestFixture.getTravelogueDayRequests(); + List requests = TravelogueRequestFixture.getTravelogueDayRequests(); Travelogue travelogue = testHelper.persistTravelogue(); Map> daysMap = dayService.createDays(requests, travelogue); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 17a0a811..4707a5f4 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -10,7 +10,8 @@ import woowacourse.touroot.global.ServiceTest; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueResponseFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; @@ -51,10 +52,10 @@ public TravelogueFacadeServiceTest( @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { - TravelogueRequest request = TravelogueTestFixture.getTravelogueRequest(); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); assertThat(service.createTravelogue(request)) - .isEqualTo(TravelogueTestFixture.getTravelogueResponse()); + .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } @DisplayName("여행기를 ID를 기준으로 조회한다.") @@ -63,14 +64,14 @@ void findTravelogueById() { testHelper.initTravelogueTestData(); assertThat(service.findTravelogueById(1L)) - .isEqualTo(TravelogueTestFixture.getTravelogueResponse()); + .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } @DisplayName("메인 페이지에 표시할 여행기 목록을 조회한다.") @Test void findTravelogues() { testHelper.initTravelogueTestData(); - Page responses = TravelogueTestFixture.getTravelogueResponses(); + Page responses = TravelogueResponseFixture.getTravelogueResponses(); assertThat(service.findTravelogues(Pageable.ofSize(5))) .isEqualTo(responses); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java index a04555d6..7f8b5dd2 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -11,7 +11,7 @@ import woowacourse.touroot.travelogue.domain.TraveloguePhoto; import woowacourse.touroot.travelogue.domain.TraveloguePlace; import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import java.util.List; @@ -35,7 +35,7 @@ public TraveloguePhotoServiceTest(TraveloguePhotoService photoService, Travelogu @DisplayName("여행기 사진을 생성한다.") @Test void createPhotos() { - List requests = TravelogueTestFixture.getTraveloguePhotoRequests(); + List requests = TravelogueRequestFixture.getTraveloguePhotoRequests(); Travelogue travelogue = testHelper.persistTravelogue(); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); Place position = testHelper.persistPlace(); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 9f1e27d6..3d241f14 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -13,7 +13,7 @@ import woowacourse.touroot.travelogue.domain.TraveloguePlace; import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; @@ -53,7 +53,7 @@ void setUp() { @DisplayName("여행기 장소를 생성한다.") @Test void createPlaces() { - List requests = TravelogueTestFixture.getTraveloguePlaceRequests(); + List requests = TravelogueRequestFixture.getTraveloguePlaceRequests(); Travelogue travelogue = testHelper.persistTravelogue(); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java index fba44e00..2cdd50fc 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java @@ -10,7 +10,7 @@ import woowacourse.touroot.global.exception.BadRequestException; import woowacourse.touroot.travelogue.domain.Travelogue; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueTestFixture; +import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; @@ -49,12 +49,12 @@ public TravelogueServiceTest( @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { - TravelogueRequest request = TravelogueTestFixture.getTravelogueRequest(); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); Travelogue createdTravelogue = travelogueService.createTravelogue(request); assertAll( () -> assertThat(createdTravelogue.getId()).isEqualTo(1L), - () -> assertThat(createdTravelogue.getTitle()).isEqualTo("낭만의 시베리아 횡단철도 여행") + () -> assertThat(createdTravelogue.getTitle()).isEqualTo("제주에 하영 옵서") ); } From 2d92a14aa9e6a250774cc6de00c74998db3c2efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 25 Jul 2024 17:15:13 +0900 Subject: [PATCH 046/108] =?UTF-8?q?[Feature]=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20Redirect=20Uri=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=20=EB=B0=B0=ED=8F=AC=20=EC=A3=BC=EC=86=8C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 698fc8e3..2d5351d8 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -3,6 +3,7 @@ oauth: user-information-request-uri: https://kapi.kakao.com/v2/user/me access-token-request-uri: https://kauth.kakao.com/oauth/token rest-api-key: ENC(i6J7NWUsDpXVbJrbSUcNFI3h0oc6v8PxuHShU9UA7EVuUNLtQN/ANII+8j5HjhGO) + redirect-uri: https://dev.touroot.kr/oauth jasypt: encryptor: algorithm: PBEWithMD5AndDES @@ -39,9 +40,6 @@ spring: hibernate: ddl-auto: create-drop defer-datasource-initialization: true -oauth: - kakao: - redirect-uri: http://localhost:3000/oauth --- security: jwt: @@ -73,6 +71,3 @@ spring: dialect: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: none -oauth: - kakao: - redirect-uri: http://localhost:3000/oauth From 4a8f155c609006743504300dd9d4e0137916fbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Fri, 26 Jul 2024 11:05:37 +0900 Subject: [PATCH 047/108] =?UTF-8?q?[Fix]=20-=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9A=A9=EB=9F=89=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20(#134)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 멀티파트 파일 업로드 용량 조정 * feat: 파일 업로드 용량 초과 예외 처리 작성 --- .../touroot/global/exception/GlobalExceptionHandler.java | 9 +++++++++ backend/src/main/resources/application.yml | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java index 6e2bd4f3..6b3c2d7b 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import woowacourse.touroot.global.exception.dto.ExceptionResponse; @Slf4j @@ -42,4 +43,12 @@ public ResponseEntity handleClientException(ClientException e ExceptionResponse data = new ExceptionResponse(exception.getMessage()); return ResponseEntity.internalServerError().body(data); } + + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity handleUploadExceedException(MaxUploadSizeExceededException exception) { + log.info("UPLOAD_SIZE_EXCEPTION :: message = {}", exception.getMessage()); + + ExceptionResponse data = new ExceptionResponse("파일 업로드 용량을 초과하였습니다."); + return ResponseEntity.badRequest().body(data); + } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 2d5351d8..0be5ad8e 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -52,6 +52,10 @@ server: key-store-password: ENC(aIkhk+PERnL/OBpa2HtNyXAjOGbCZx9TJ+L5sekBxdI=) key-store: ENC(7VQCNdI7mXATwc4AiymZoyf3mz9SiskXpLnenpMSFBI=) spring: + servlet: + multipart: + max-file-size: 5MB + max-request-size: 50MB config: activate: on-profile: dev From 245bc34052a6bc9b6aa035dc5236b736dca52183 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Fri, 26 Jul 2024 11:18:42 +0900 Subject: [PATCH 048/108] =?UTF-8?q?[Fix]=20-=20Spring=20Boot=20Timezone=20?= =?UTF-8?q?KST=EB=A1=9C=20=EC=84=A4=EC=A0=95=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index b765de14..310ffcfa 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ FROM openjdk:17-oracle ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "/app.jar"] +ENTRYPOINT ["java", "-jar", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=dev", "/app.jar"] From a6acb989a126de8e2faee0e909d4585f4a3940f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:44:12 +0900 Subject: [PATCH 049/108] =?UTF-8?q?[Fix]=20-=20HEIC=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=8C=8C=EC=9D=BC=20=ED=99=95=EC=9E=A5=EC=9E=90=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/woowacourse/touroot/image/domain/ImageFile.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java b/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java index 68b67965..00705be3 100644 --- a/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java +++ b/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java @@ -8,7 +8,7 @@ @Getter public class ImageFile { - private static final List WHITE_LIST = List.of("jpg", "jpeg", "png", "webp"); + private static final List WHITE_LIST = List.of("jpg", "jpeg", "png", "webp", "heic"); private final MultipartFile file; @@ -35,7 +35,7 @@ public void validateExtension(String fileName) { throw new BadRequestException("파일 형식이 잘못되었습니다."); } String extension = fileName.substring(extensionIndex + 1); - if (!WHITE_LIST.contains(extension)) { + if (!WHITE_LIST.contains(extension.toLowerCase())) { throw new BadRequestException("파일 형식이 잘못되었습니다."); } } From ea36b0f89cbfd4e83a2dab2c4f8411c4cab4a87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Mon, 29 Jul 2024 21:33:13 +0900 Subject: [PATCH 050/108] =?UTF-8?q?[Feature]=20-=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=20=EB=A1=9C=EC=BB=AC=20=EA=B0=9C=EB=B0=9C=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20Oauth=20redirect=20uri=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 0be5ad8e..55a7430c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -3,7 +3,7 @@ oauth: user-information-request-uri: https://kapi.kakao.com/v2/user/me access-token-request-uri: https://kauth.kakao.com/oauth/token rest-api-key: ENC(i6J7NWUsDpXVbJrbSUcNFI3h0oc6v8PxuHShU9UA7EVuUNLtQN/ANII+8j5HjhGO) - redirect-uri: https://dev.touroot.kr/oauth + redirect-uri: http://localhost:3000/oauth jasypt: encryptor: algorithm: PBEWithMD5AndDES From 8f0fd60fb89a207a8c875a3a7525a03f658c1f64 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:59:45 +0900 Subject: [PATCH 051/108] =?UTF-8?q?[Feature]=20-=20=EC=9D=B8=EA=B0=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: UnauthorizedException 추가 * feat: Swagger에 Authorization 추가 * feat: Authorization Header 검증용 filter 구현 * feat: 로그인된 사요앚 정보 얻기 위한 ArgumentResolver 구현 * test: TravelPlanController 테스트에 accessToken 추가 * refactor: 필터에서 token이 없을 경우의 에러메시지 수정 * refactor: JwtTokenProvider 예외 발생 시 cause 포함되게 수정 * refactor: JwtAuthFilter else문 제거 --- .../infrastructure/JwtTokenProvider.java | 22 ++++- .../touroot/global/auth/JwtAuthFilter.java | 83 +++++++++++++++++++ .../MemberAuthMethodArgumentResolver.java | 31 +++++++ .../global/auth/dto/HttpRequestInfo.java | 6 ++ .../touroot/global/auth/dto/MemberAuth.java | 4 + .../touroot/global/config/SwaggerConfig.java | 16 +++- .../touroot/global/config/WebMvcConfig.java | 21 +++++ .../exception/UnauthorizedException.java | 12 +++ .../controller/LoginControllerTest.java | 17 ++-- .../controller/TravelogueControllerTest.java | 15 +++- .../helper/TravelogueTestHelper.java | 24 ++++-- .../controller/TravelPlanControllerTest.java | 19 ++++- .../helper/TravelPlanTestHelper.java | 16 +++- 13 files changed, 263 insertions(+), 23 deletions(-) create mode 100644 backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/auth/dto/HttpRequestInfo.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/auth/dto/MemberAuth.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java create mode 100644 backend/src/main/java/woowacourse/touroot/global/exception/UnauthorizedException.java diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java index 383d1ce6..86e5d800 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java @@ -1,10 +1,14 @@ package woowacourse.touroot.authentication.infrastructure; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; -import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import woowacourse.touroot.global.exception.UnauthorizedException; + +import java.nio.charset.StandardCharsets; +import java.util.Date; @Component public class JwtTokenProvider { @@ -33,4 +37,20 @@ public String createToken(Long memberId) { .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) .compact(); } + + public String decode(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) + .build() + .parseClaimsJws(token) + .getBody() + .get(MEMBER_ID_KEY) + .toString(); + } catch (ExpiredJwtException exception) { + throw new UnauthorizedException("이미 만료된 토큰입니다.", exception.getCause()); + } catch (Exception exception) { + throw new UnauthorizedException("유효하지 않은 토큰입니다.", exception.getCause()); + } + } } diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java new file mode 100644 index 00000000..fd6bcef6 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java @@ -0,0 +1,83 @@ +package woowacourse.touroot.global.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; +import woowacourse.touroot.global.auth.dto.HttpRequestInfo; +import woowacourse.touroot.global.exception.dto.ExceptionResponse; + +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +@Slf4j +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + private final JwtTokenProvider tokenProvider; + + private static final List WHITE_LIST = List.of( + new HttpRequestInfo(HttpMethod.GET, "/ping"), + new HttpRequestInfo(HttpMethod.GET, "/h2-console/**"), + new HttpRequestInfo(HttpMethod.POST, "/h2-console/**"), + new HttpRequestInfo(HttpMethod.GET, "/favicon/**"), + new HttpRequestInfo(HttpMethod.GET, "/swagger-ui/**"), + new HttpRequestInfo(HttpMethod.GET, "/swagger-resources/**"), + new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), + new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), + new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/**"), + new HttpRequestInfo(HttpMethod.GET, "/api/v1/login/**") + ); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + if (token == null) { + sendUnauthorizedResponse(response, "로그인을 해주세요."); + return; + } + + token = token.split("Bearer|bearer")[1]; + try { + String memberId = tokenProvider.decode(token); + request.setAttribute("memberId", memberId); + filterChain.doFilter(request, response); + } catch (Exception e) { + sendUnauthorizedResponse(response, e.getMessage()); + } + } + + private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException { + ExceptionResponse errorResponse = new ExceptionResponse(message); + + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter() + .write(objectMapper.writeValueAsString(errorResponse)); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + + String url = request.getRequestURI(); + String method = request.getMethod(); + + return WHITE_LIST.stream() + .anyMatch(white -> white.method().matches(method) && antPathMatcher.match(white.urlPattern(), url)); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java b/backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java new file mode 100644 index 00000000..3c479e94 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java @@ -0,0 +1,31 @@ +package woowacourse.touroot.global.auth; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import woowacourse.touroot.global.auth.dto.MemberAuth; + +@Component +public class MemberAuthMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(MemberAuth.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + String memberId = request.getAttribute("memberId").toString(); + return new MemberAuth(Long.valueOf(memberId)); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/dto/HttpRequestInfo.java b/backend/src/main/java/woowacourse/touroot/global/auth/dto/HttpRequestInfo.java new file mode 100644 index 00000000..ec22bfb8 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/auth/dto/HttpRequestInfo.java @@ -0,0 +1,6 @@ +package woowacourse.touroot.global.auth.dto; + +import org.springframework.http.HttpMethod; + +public record HttpRequestInfo(HttpMethod method, String urlPattern) { +} diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/dto/MemberAuth.java b/backend/src/main/java/woowacourse/touroot/global/auth/dto/MemberAuth.java new file mode 100644 index 00000000..0bdc2d86 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/auth/dto/MemberAuth.java @@ -0,0 +1,4 @@ +package woowacourse.touroot.global.auth.dto; + +public record MemberAuth(Long memberId) { +} diff --git a/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java index 3870939a..e0b2b63f 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java +++ b/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java @@ -1,9 +1,13 @@ package woowacourse.touroot.global.config; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; @Configuration public class SwaggerConfig { @@ -11,7 +15,9 @@ public class SwaggerConfig { @Bean public OpenAPI createOpenApi() { return new OpenAPI() - .info(getInfo()); + .info(getInfo()) + .components(new Components().addSecuritySchemes("bearerAuth", getSecurityScheme())) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")); } private Info getInfo() { @@ -20,4 +26,12 @@ private Info getInfo() { .description("To your route, 투룻 API") .version("0.1"); } + + private SecurityScheme getSecurityScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .in(SecurityScheme.In.HEADER) + .name(HttpHeaders.AUTHORIZATION); + } } diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java new file mode 100644 index 00000000..39b885c2 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java @@ -0,0 +1,21 @@ +package woowacourse.touroot.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import woowacourse.touroot.global.auth.MemberAuthMethodArgumentResolver; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final MemberAuthMethodArgumentResolver memberAuthMethodArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(memberAuthMethodArgumentResolver); + } +} diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/UnauthorizedException.java b/backend/src/main/java/woowacourse/touroot/global/exception/UnauthorizedException.java new file mode 100644 index 00000000..5b7fed83 --- /dev/null +++ b/backend/src/main/java/woowacourse/touroot/global/exception/UnauthorizedException.java @@ -0,0 +1,12 @@ +package woowacourse.touroot.global.exception; + +public class UnauthorizedException extends RuntimeException { + + public UnauthorizedException(String message) { + super(message); + } + + public UnauthorizedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java index bc34d81a..6faaa671 100644 --- a/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java @@ -1,23 +1,24 @@ package woowacourse.touroot.authentication.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import woowacourse.touroot.authentication.dto.response.LoginResponse; import woowacourse.touroot.authentication.service.LoginService; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Disabled @WebMvcTest(LoginController.class) class LoginControllerTest { @@ -27,8 +28,6 @@ class LoginControllerTest { private ObjectMapper objectMapper; @MockBean private LoginService loginService; - @MockBean - JpaMetamodelMappingContext jpaMetamodelMappingContext; @DisplayName("카카오 로그인 요청을 처리할 수 있다") @Test diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java index bcfba99b..cdaf34a9 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java @@ -1,7 +1,5 @@ package woowacourse.touroot.travelogue.controller; -import static org.hamcrest.Matchers.is; - import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; @@ -12,7 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; import woowacourse.touroot.global.AcceptanceTest; +import woowacourse.touroot.member.domain.Member; import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; @@ -20,6 +21,8 @@ import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; import woowacourse.touroot.utils.DatabaseCleaner; +import static org.hamcrest.Matchers.is; + @DisplayName("여행기 컨트롤러") @AcceptanceTest class TravelogueControllerTest { @@ -29,16 +32,19 @@ class TravelogueControllerTest { private final DatabaseCleaner databaseCleaner; private final TravelogueTestHelper testHelper; private final ObjectMapper objectMapper; + private final JwtTokenProvider jwtTokenProvider; @Autowired public TravelogueControllerTest( DatabaseCleaner databaseCleaner, TravelogueTestHelper testHelper, - ObjectMapper objectMapper + ObjectMapper objectMapper, + JwtTokenProvider jwtTokenProvider ) { this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; this.objectMapper = objectMapper; + this.jwtTokenProvider = jwtTokenProvider; } @BeforeEach @@ -52,9 +58,12 @@ void setUp() { @Test void createTravelogue() { TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); + Member member = testHelper.initMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(request) .when().post("/api/v1/travelogues") .then().log().all() diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java index 26fa6d3a..1c36e973 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,13 +1,9 @@ package woowacourse.touroot.travelogue.helper; -import static woowacourse.touroot.place.fixture.PlaceFixture.PLACE; -import static woowacourse.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; -import static woowacourse.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; -import static woowacourse.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; -import static woowacourse.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import woowacourse.touroot.member.domain.Member; +import woowacourse.touroot.member.repository.MemberRepository; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; import woowacourse.touroot.travelogue.domain.Travelogue; @@ -19,6 +15,12 @@ import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; import woowacourse.touroot.travelogue.repository.TravelogueRepository; +import static woowacourse.touroot.place.fixture.PlaceFixture.PLACE; +import static woowacourse.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; +import static woowacourse.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; +import static woowacourse.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; +import static woowacourse.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; + @Component public class TravelogueTestHelper { @@ -27,6 +29,7 @@ public class TravelogueTestHelper { private final TravelogueDayRepository travelogueDayRepository; private final TraveloguePlaceRepository traveloguePlaceRepository; private final TraveloguePhotoRepository traveloguePhotoRepository; + private final MemberRepository memberRepository; @Autowired public TravelogueTestHelper( @@ -34,13 +37,15 @@ public TravelogueTestHelper( TravelogueRepository travelogueRepository, TravelogueDayRepository travelogueDayRepository, TraveloguePlaceRepository traveloguePlaceRepository, - TraveloguePhotoRepository traveloguePhotoRepository + TraveloguePhotoRepository traveloguePhotoRepository, + MemberRepository memberRepository ) { this.placeRepository = placeRepository; this.travelogueRepository = travelogueRepository; this.travelogueDayRepository = travelogueDayRepository; this.traveloguePlaceRepository = traveloguePlaceRepository; this.traveloguePhotoRepository = traveloguePhotoRepository; + this.memberRepository = memberRepository; } public void initTravelogueTestData() { @@ -80,4 +85,9 @@ public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { return traveloguePhotoRepository.save(photo); } + + public Member initMemberTestData() { + Member member = new Member(1L, "tester", "image"); + return memberRepository.save(member); + } } diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java index cf177078..94417828 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -7,7 +7,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; +import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; import woowacourse.touroot.global.AcceptanceTest; +import woowacourse.touroot.member.domain.Member; import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import woowacourse.touroot.travelplan.dto.request.PlanPositionCreateRequest; @@ -27,12 +30,18 @@ class TravelPlanControllerTest { @LocalServerPort private int port; private final DatabaseCleaner databaseCleaner; + private final JwtTokenProvider jwtTokenProvider; private final TravelPlanTestHelper testHelper; @Autowired - public TravelPlanControllerTest(DatabaseCleaner databaseCleaner, TravelPlanTestHelper testHelper) { + public TravelPlanControllerTest( + DatabaseCleaner databaseCleaner, + TravelPlanTestHelper testHelper, + JwtTokenProvider jwtTokenProvider + ) { this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; + this.jwtTokenProvider = jwtTokenProvider; } @BeforeEach @@ -58,9 +67,13 @@ void createTravelPlan() { .days(List.of(planDayCreateRequest)) .build(); + Member member = testHelper.initMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + // when & then RestAssured.given().log().all() .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(request) .when().log().all() .post("/api/v1/travel-plans") @@ -86,9 +99,13 @@ void createTravelPlanWithInvalidStartDate() { .days(List.of(planDayCreateRequest)) .build(); + Member member = testHelper.initMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + // when & then RestAssured.given().log().all() .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(request) .when().log().all() .post("/api/v1/travel-plans") diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java index ae60ba23..26242d35 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -2,6 +2,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import woowacourse.touroot.member.domain.Member; +import woowacourse.touroot.member.repository.MemberRepository; import woowacourse.touroot.place.domain.Place; import woowacourse.touroot.place.repository.PlaceRepository; import woowacourse.touroot.travelplan.domain.TravelPlan; @@ -20,18 +22,25 @@ public class TravelPlanTestHelper { private final TravelPlanRepository travelPlanRepository; private final TravelPlanDayRepository travelPlanDayRepository; private final TravelPlanPlaceRepository travelPlanPlaceRepository; + private final MemberRepository memberRepository; @Autowired public TravelPlanTestHelper( PlaceRepository placeRepository, TravelPlanRepository travelPlanRepository, TravelPlanDayRepository travelPlanDayRepository, - TravelPlanPlaceRepository travelPlanPlaceRepository + TravelPlanPlaceRepository travelPlanPlaceRepository, + MemberRepository memberRepository ) { this.placeRepository = placeRepository; this.travelPlanRepository = travelPlanRepository; this.travelPlanDayRepository = travelPlanDayRepository; this.travelPlanPlaceRepository = travelPlanPlaceRepository; + this.memberRepository = memberRepository; + } + + public static Member getMember(Long kakaoId, String nickname, String profileImageUri) { + return new Member(kakaoId, nickname, profileImageUri); } public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { @@ -61,4 +70,9 @@ public void initTravelPlanTestData() { placeRepository.save(place); travelPlanPlaceRepository.save(travelPlanPlace); } + + public Member initMemberTestData() { + Member member = getMember(1L, "tester", "image"); + return memberRepository.save(member); + } } From 46d276abfc03d63b526e362a40391eec863ac30f Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:21:07 +0900 Subject: [PATCH 052/108] =?UTF-8?q?fix:=20JwtAuthFilter=20white=20list=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20method=20post=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/woowacourse/touroot/global/auth/JwtAuthFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java index fd6bcef6..d85fbfc8 100644 --- a/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java @@ -38,7 +38,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/**"), - new HttpRequestInfo(HttpMethod.GET, "/api/v1/login/**") + new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**") ); @Override From 3d5c16b4625432109f966a0e838e1b10ffa0a803 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:41:11 +0900 Subject: [PATCH 053/108] =?UTF-8?q?[Fix]=20-=20Authorization=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: JwtAuthFilter white list 로그인 method post로 변경 * fix: CORS 설정에 Authorization header 추가 --- .../java/woowacourse/touroot/global/config/WebConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java index e32b6469..811ee10a 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java @@ -1,6 +1,7 @@ package woowacourse.touroot.global.config; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -10,6 +11,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr"); + .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr") + .allowedHeaders(HttpHeaders.AUTHORIZATION); } } From 81d86ef04b62ee4f6303319f5874b4b74dbf2f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Tue, 30 Jul 2024 15:53:38 +0900 Subject: [PATCH 054/108] =?UTF-8?q?[Feature]=20-=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=80=EC=A6=9D=20=EA=B7=9C=EC=B9=99=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 멤버 생성 dto속 메서드로 캡슐화 * refactor: BaseEntity 패키지 global로 수정 * refactor: 스웨거 문서 컨벤션 통일 * refactor: 이미지 파일 예외 메시지 구체화 개선 * refactor: 유효성 검사 조건이 없는 파라미터의 애너테이션 제거 및 공백 컨벤션 통일 개선 * feat: ImageFile 도메인 검증 추가 - notnull - filename not blank * test: ImageFile 도메인 테스트 작성 * feat: 멤버 검증 로직 추가 - 필드 not null - 문자열 필드 not blank * feat: 멤버 닉네임 길이 검증 추가 - 1-20자 닉네임 허용 * test: 멤버 도메인 테스트 작성 * fix: 보안 규칙에 따른 패키지명 수정 --- .../touroot/TourootApplication.java | 2 +- .../controller/LoginController.java | 8 +- .../dto/response/LoginResponse.java | 4 +- .../OauthUserInformationResponse.java | 9 ++- .../kakao/KakaoAccessTokenResponse.java | 2 +- .../dto/response/kakao/KakaoAccount.java | 2 +- .../dto/response/kakao/KakaoProfile.java | 2 +- .../infrastructure/JwtTokenProvider.java | 2 +- .../infrastructure/KakaoOauthClient.java | 10 +-- .../infrastructure/KakaoOauthProvider.java | 4 +- .../authentication/service/LoginService.java | 18 ++--- .../touroot/global/auth/JwtAuthFilter.java | 9 +-- .../MemberAuthMethodArgumentResolver.java | 2 +- .../global/auth/dto/HttpRequestInfo.java | 0 .../touroot/global/auth/dto/MemberAuth.java | 0 .../touroot/global/config/JasyptConfig.java | 2 +- .../touroot/global/config/SwaggerConfig.java | 2 +- .../touroot/global/config/WebConfig.java | 2 +- .../touroot/global/config/WebMvcConfig.java | 7 +- .../touroot/global}/entity/BaseEntity.java | 2 +- .../global/exception/BadRequestException.java | 2 +- .../global/exception/ClientException.java | 2 +- .../exception/GlobalExceptionHandler.java | 4 +- .../exception/UnauthorizedException.java | 0 .../exception/dto/ExceptionResponse.java | 2 +- .../image/controller/ImageController.java | 10 ++- .../touroot/image/domain/ImageFile.java | 27 ++++--- .../image/infrastructure/AwsS3Provider.java | 6 +- .../touroot/image/service/ImageService.java | 6 +- .../java/kr/touroot/member/domain/Member.java | 71 +++++++++++++++++ .../member/repository/MemberRepository.java | 4 +- .../touroot/place/domain/Place.java | 14 ++-- .../place/repository/PlaceRepository.java | 7 +- .../controller/TravelogueController.java | 13 ++-- .../touroot/travelogue/domain/Travelogue.java | 4 +- .../travelogue/domain/TravelogueDay.java | 4 +- .../travelogue/domain/TraveloguePhoto.java | 4 +- .../travelogue/domain/TraveloguePlace.java | 6 +- .../dto/request/TravelogueDayRequest.java | 6 +- .../dto/request/TraveloguePhotoRequest.java | 6 +- .../dto/request/TraveloguePlaceRequest.java | 8 +- .../request/TraveloguePositionRequest.java | 2 +- .../dto/request/TravelogueRequest.java | 4 +- .../dto/response/TravelogueDayResponse.java | 4 +- .../dto/response/TraveloguePlaceResponse.java | 4 +- .../response/TraveloguePositionResponse.java | 4 +- .../dto/response/TravelogueResponse.java | 4 +- .../repository/TravelogueDayRepository.java | 6 +- .../repository/TraveloguePhotoRepository.java | 6 +- .../repository/TraveloguePlaceRepository.java | 6 +- .../repository/TravelogueRepository.java | 4 +- .../service/TravelogueDayService.java | 14 ++-- .../service/TravelogueFacadeService.java | 24 +++--- .../service/TraveloguePhotoService.java | 10 +-- .../service/TraveloguePlaceService.java | 18 ++--- .../travelogue/service/TravelogueService.java | 10 +-- .../controller/TravelPlanController.java | 19 +++-- .../touroot/travelplan/domain/TravelPlan.java | 15 ++-- .../travelplan/domain/TravelPlanDay.java | 18 +++-- .../travelplan/domain/TravelPlanPlace.java | 17 ++-- .../dto/request/PlanDayCreateRequest.java | 7 +- .../dto/request/PlanPlaceCreateRequest.java | 8 +- .../request/PlanPositionCreateRequest.java | 2 +- .../dto/request/TravelPlanCreateRequest.java | 7 +- .../response/TravelPlanCreateResponse.java | 2 +- .../dto/response/TravelPlanDayResponse.java | 4 +- .../dto/response/TravelPlanPlaceResponse.java | 6 +- .../response/TravelPlanPositionResponse.java | 4 +- .../dto/response/TravelPlanResponse.java | 7 +- .../repository/TravelPlanDayRepository.java | 9 +-- .../repository/TravelPlanPlaceRepository.java | 9 +-- .../repository/TravelPlanRepository.java | 4 +- .../travelplan/service/TravelPlanService.java | 39 +++++----- .../touroot/member/domain/Member.java | 36 --------- .../controller/LoginControllerTest.java | 23 +++--- .../authentication/fixture/MemberFixture.java | 4 +- .../fixture/OauthUserInformationFixture.java | 12 +++ .../infrastructure/JwtTokenProviderTest.java | 2 +- .../service/LoginServiceTest.java | 16 ++-- .../touroot/global/AcceptanceTest.java | 2 +- .../touroot/global/ServiceTest.java | 9 +-- .../touroot/image/domain/ImageFileTest.java | 69 +++++++++++++++++ .../kr/touroot/member/domain/MemberTest.java | 77 +++++++++++++++++++ .../touroot/place/fixture/PlaceFixture.java | 4 +- .../controller/TravelogueControllerTest.java | 24 +++--- .../fixture/TravelogueDayFixture.java | 8 +- .../travelogue/fixture/TravelogueFixture.java | 4 +- .../fixture/TraveloguePhotoFixture.java | 8 +- .../fixture/TraveloguePlaceFixture.java | 12 +-- .../fixture/TravelogueRequestFixture.java | 12 +-- .../fixture/TravelogueResponseFixture.java | 10 +-- .../helper/TravelogueTestHelper.java | 40 +++++----- .../service/TravelogueDayServiceTest.java | 35 ++++----- .../service/TravelogueFacadeServiceTest.java | 20 ++--- .../service/TraveloguePhotoServiceTest.java | 27 ++++--- .../service/TraveloguePlaceServiceTest.java | 24 +++--- .../service/TravelogueServiceTest.java | 26 +++---- .../controller/TravelPlanControllerTest.java | 29 ++++--- .../travelplan/domain/TravelPlanTest.java | 11 ++- .../helper/TravelPlanTestHelper.java | 25 +++--- .../service/TravelPlanServiceTest.java | 33 ++++---- .../touroot/utils/DatabaseCleaner.java | 2 +- .../fixture/OauthUserInformationFixture.java | 12 --- 103 files changed, 706 insertions(+), 502 deletions(-) rename backend/src/main/java/{woowacourse => kr}/touroot/TourootApplication.java (93%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/controller/LoginController.java (86%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/dto/response/LoginResponse.java (85%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/dto/response/OauthUserInformationResponse.java (61%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java (88%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/dto/response/kakao/KakaoAccount.java (68%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/dto/response/kakao/KakaoProfile.java (75%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/infrastructure/JwtTokenProvider.java (97%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/infrastructure/KakaoOauthClient.java (91%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/infrastructure/KakaoOauthProvider.java (72%) rename backend/src/main/java/{woowacourse => kr}/touroot/authentication/service/LoginService.java (54%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/auth/JwtAuthFilter.java (95%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/auth/MemberAuthMethodArgumentResolver.java (96%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/auth/dto/HttpRequestInfo.java (100%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/auth/dto/MemberAuth.java (100%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/config/JasyptConfig.java (83%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/config/SwaggerConfig.java (96%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/config/WebConfig.java (93%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/config/WebMvcConfig.java (84%) rename backend/src/main/java/{woowacourse/touroot => kr/touroot/global}/entity/BaseEntity.java (94%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/exception/BadRequestException.java (75%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/exception/ClientException.java (74%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/exception/GlobalExceptionHandler.java (95%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/exception/UnauthorizedException.java (100%) rename backend/src/main/java/{woowacourse => kr}/touroot/global/exception/dto/ExceptionResponse.java (51%) rename backend/src/main/java/{woowacourse => kr}/touroot/image/controller/ImageController.java (83%) rename backend/src/main/java/{woowacourse => kr}/touroot/image/domain/ImageFile.java (53%) rename backend/src/main/java/{woowacourse => kr}/touroot/image/infrastructure/AwsS3Provider.java (94%) rename backend/src/main/java/{woowacourse => kr}/touroot/image/service/ImageService.java (76%) create mode 100644 backend/src/main/java/kr/touroot/member/domain/Member.java rename backend/src/main/java/{woowacourse => kr}/touroot/member/repository/MemberRepository.java (69%) rename backend/src/main/java/{woowacourse => kr}/touroot/place/domain/Place.java (76%) rename backend/src/main/java/{woowacourse => kr}/touroot/place/repository/PlaceRepository.java (73%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/controller/TravelogueController.java (89%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/domain/Travelogue.java (90%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/domain/TravelogueDay.java (91%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/domain/TraveloguePhoto.java (92%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/domain/TraveloguePlace.java (91%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/request/TravelogueDayRequest.java (76%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/request/TraveloguePhotoRequest.java (74%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/request/TraveloguePlaceRequest.java (85%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/request/TraveloguePositionRequest.java (90%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/request/TravelogueRequest.java (88%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/response/TravelogueDayResponse.java (83%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/response/TraveloguePlaceResponse.java (89%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/response/TraveloguePositionResponse.java (83%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/dto/response/TravelogueResponse.java (92%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/repository/TravelogueDayRepository.java (58%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/repository/TraveloguePhotoRepository.java (59%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/repository/TraveloguePlaceRepository.java (59%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/repository/TravelogueRepository.java (57%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/service/TravelogueDayService.java (74%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/service/TravelogueFacadeService.java (82%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/service/TraveloguePhotoService.java (78%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/service/TraveloguePlaceService.java (75%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelogue/service/TravelogueService.java (72%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/controller/TravelPlanController.java (74%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/domain/TravelPlan.java (71%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/domain/TravelPlanDay.java (68%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/domain/TravelPlanPlace.java (67%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/request/PlanDayCreateRequest.java (79%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java (81%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/request/PlanPositionCreateRequest.java (90%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/request/TravelPlanCreateRequest.java (91%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/response/TravelPlanCreateResponse.java (76%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/response/TravelPlanDayResponse.java (84%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java (83%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/response/TravelPlanPositionResponse.java (83%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/dto/response/TravelPlanResponse.java (88%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/repository/TravelPlanDayRepository.java (58%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/repository/TravelPlanPlaceRepository.java (57%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/repository/TravelPlanRepository.java (57%) rename backend/src/main/java/{woowacourse => kr}/touroot/travelplan/service/TravelPlanService.java (74%) delete mode 100644 backend/src/main/java/woowacourse/touroot/member/domain/Member.java rename backend/src/test/java/{woowacourse => kr}/touroot/authentication/controller/LoginControllerTest.java (84%) rename backend/src/test/java/{woowacourse => kr}/touroot/authentication/fixture/MemberFixture.java (54%) create mode 100644 backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java rename backend/src/test/java/{woowacourse => kr}/touroot/authentication/infrastructure/JwtTokenProviderTest.java (92%) rename backend/src/test/java/{woowacourse => kr}/touroot/authentication/service/LoginServiceTest.java (82%) rename backend/src/test/java/{woowacourse => kr}/touroot/global/AcceptanceTest.java (93%) rename backend/src/test/java/{woowacourse => kr}/touroot/global/ServiceTest.java (88%) create mode 100644 backend/src/test/java/kr/touroot/image/domain/ImageFileTest.java create mode 100644 backend/src/test/java/kr/touroot/member/domain/MemberTest.java rename backend/src/test/java/{woowacourse => kr}/touroot/place/fixture/PlaceFixture.java (86%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/controller/TravelogueControllerTest.java (85%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/fixture/TravelogueDayFixture.java (67%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/fixture/TravelogueFixture.java (79%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/fixture/TraveloguePhotoFixture.java (69%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/fixture/TraveloguePlaceFixture.java (66%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/fixture/TravelogueRequestFixture.java (74%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/fixture/TravelogueResponseFixture.java (85%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/helper/TravelogueTestHelper.java (68%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/service/TravelogueDayServiceTest.java (82%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/service/TravelogueFacadeServiceTest.java (82%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/service/TraveloguePhotoServiceTest.java (77%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/service/TraveloguePlaceServiceTest.java (81%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelogue/service/TravelogueServiceTest.java (84%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelplan/controller/TravelPlanControllerTest.java (89%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelplan/domain/TravelPlanTest.java (85%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelplan/helper/TravelPlanTestHelper.java (79%) rename backend/src/test/java/{woowacourse => kr}/touroot/travelplan/service/TravelPlanServiceTest.java (85%) rename backend/src/test/java/{woowacourse => kr}/touroot/utils/DatabaseCleaner.java (98%) delete mode 100644 backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java diff --git a/backend/src/main/java/woowacourse/touroot/TourootApplication.java b/backend/src/main/java/kr/touroot/TourootApplication.java similarity index 93% rename from backend/src/main/java/woowacourse/touroot/TourootApplication.java rename to backend/src/main/java/kr/touroot/TourootApplication.java index 1106428e..3eb9029d 100644 --- a/backend/src/main/java/woowacourse/touroot/TourootApplication.java +++ b/backend/src/main/java/kr/touroot/TourootApplication.java @@ -1,4 +1,4 @@ -package woowacourse.touroot; +package kr.touroot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java similarity index 86% rename from backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java rename to backend/src/main/java/kr/touroot/authentication/controller/LoginController.java index e3aee7b5..a0a737c2 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.controller; +package kr.touroot.authentication.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -6,15 +6,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import kr.touroot.authentication.dto.response.LoginResponse; +import kr.touroot.authentication.service.LoginService; +import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import woowacourse.touroot.authentication.dto.response.LoginResponse; -import woowacourse.touroot.authentication.service.LoginService; -import woowacourse.touroot.global.exception.dto.ExceptionResponse; @Tag(name = "로그인") @RequiredArgsConstructor diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/LoginResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java similarity index 85% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/response/LoginResponse.java rename to backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java index f5abc640..6bda6ff1 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/LoginResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java @@ -1,7 +1,7 @@ -package woowacourse.touroot.authentication.dto.response; +package kr.touroot.authentication.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import woowacourse.touroot.member.domain.Member; +import kr.touroot.member.domain.Member; public record LoginResponse( @Schema(description = "로그인된 유저의 닉네임", example = "리비") String nickname, diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java similarity index 61% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java rename to backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java index 5d797078..ceab39de 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/OauthUserInformationResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java @@ -1,7 +1,8 @@ -package woowacourse.touroot.authentication.dto.response; +package kr.touroot.authentication.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import woowacourse.touroot.authentication.dto.response.kakao.KakaoAccount; +import kr.touroot.authentication.dto.response.kakao.KakaoAccount; +import kr.touroot.member.domain.Member; public record OauthUserInformationResponse( @JsonProperty("id") @@ -10,6 +11,10 @@ public record OauthUserInformationResponse( KakaoAccount kakaoAccount ) { + public Member toMember() { + return new Member(socialLoginId, nickname(), profileImage()); + } + public String nickname() { return kakaoAccount.kakaoProfile().nickname(); } diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java similarity index 88% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java rename to backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java index d6a8f986..a1277ddc 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoAccessTokenResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.dto.response.kakao; +package kr.touroot.authentication.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java b/backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoAccount.java similarity index 68% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java rename to backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoAccount.java index b025be37..b099c68a 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoAccount.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoAccount.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.dto.response.kakao; +package kr.touroot.authentication.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java b/backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoProfile.java similarity index 75% rename from backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java rename to backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoProfile.java index 7451c4a0..9fc3a7bc 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/dto/response/kakao/KakaoProfile.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/kakao/KakaoProfile.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.dto.response.kakao; +package kr.touroot.authentication.dto.response.kakao; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java similarity index 97% rename from backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java rename to backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java index 86e5d800..4cd7ec76 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.infrastructure; +package kr.touroot.authentication.infrastructure; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java similarity index 91% rename from backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java rename to backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java index bf76c598..7f424f59 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthClient.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java @@ -1,7 +1,11 @@ -package woowacourse.touroot.authentication.infrastructure; +package kr.touroot.authentication.infrastructure; import java.io.IOException; import java.time.Duration; +import kr.touroot.authentication.dto.response.OauthUserInformationResponse; +import kr.touroot.authentication.dto.response.kakao.KakaoAccessTokenResponse; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ClientException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; @@ -15,10 +19,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; -import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; -import woowacourse.touroot.authentication.dto.response.kakao.KakaoAccessTokenResponse; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.global.exception.ClientException; @Component public class KakaoOauthClient { diff --git a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java similarity index 72% rename from backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java rename to backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java index e0c75901..793212c8 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/infrastructure/KakaoOauthProvider.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java @@ -1,8 +1,8 @@ -package woowacourse.touroot.authentication.infrastructure; +package kr.touroot.authentication.infrastructure; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; +import kr.touroot.authentication.dto.response.OauthUserInformationResponse; @RequiredArgsConstructor @Component diff --git a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java similarity index 54% rename from backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java rename to backend/src/main/java/kr/touroot/authentication/service/LoginService.java index a380b6ee..48804db3 100644 --- a/backend/src/main/java/woowacourse/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java @@ -1,13 +1,13 @@ -package woowacourse.touroot.authentication.service; +package kr.touroot.authentication.service; +import kr.touroot.authentication.dto.response.LoginResponse; +import kr.touroot.authentication.dto.response.OauthUserInformationResponse; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.authentication.infrastructure.KakaoOauthProvider; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import woowacourse.touroot.authentication.dto.response.LoginResponse; -import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; -import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; -import woowacourse.touroot.authentication.infrastructure.KakaoOauthProvider; -import woowacourse.touroot.member.domain.Member; -import woowacourse.touroot.member.repository.MemberRepository; @Service @RequiredArgsConstructor @@ -26,8 +26,6 @@ public LoginResponse login(String code) { } private Member signUp(OauthUserInformationResponse userInformation) { - return memberRepository.save( - new Member(userInformation.socialLoginId(), userInformation.nickname(), userInformation.profileImage()) - ); + return memberRepository.save(userInformation.toMember()); } } diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java similarity index 95% rename from backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java rename to backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index d85fbfc8..bc1d767b 100644 --- a/backend/src/main/java/woowacourse/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -5,6 +5,10 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; @@ -13,12 +17,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; import woowacourse.touroot.global.auth.dto.HttpRequestInfo; -import woowacourse.touroot.global.exception.dto.ExceptionResponse; - -import java.io.IOException; -import java.util.List; @RequiredArgsConstructor @Slf4j diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java similarity index 96% rename from backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java rename to backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java index 3c479e94..c0773b67 100644 --- a/backend/src/main/java/woowacourse/touroot/global/auth/MemberAuthMethodArgumentResolver.java +++ b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.auth; +package kr.touroot.global.auth; import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.MethodParameter; diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/dto/HttpRequestInfo.java b/backend/src/main/java/kr/touroot/global/auth/dto/HttpRequestInfo.java similarity index 100% rename from backend/src/main/java/woowacourse/touroot/global/auth/dto/HttpRequestInfo.java rename to backend/src/main/java/kr/touroot/global/auth/dto/HttpRequestInfo.java diff --git a/backend/src/main/java/woowacourse/touroot/global/auth/dto/MemberAuth.java b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java similarity index 100% rename from backend/src/main/java/woowacourse/touroot/global/auth/dto/MemberAuth.java rename to backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java diff --git a/backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java b/backend/src/main/java/kr/touroot/global/config/JasyptConfig.java similarity index 83% rename from backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java rename to backend/src/main/java/kr/touroot/global/config/JasyptConfig.java index 2e9779c8..546ba8c7 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/JasyptConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/JasyptConfig.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.config; +package kr.touroot.global.config; import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java b/backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java similarity index 96% rename from backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java rename to backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java index e0b2b63f..23525c21 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/SwaggerConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.config; +package kr.touroot.global.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java b/backend/src/main/java/kr/touroot/global/config/WebConfig.java similarity index 93% rename from backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java rename to backend/src/main/java/kr/touroot/global/config/WebConfig.java index 811ee10a..bf0c2cb3 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebConfig.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.config; +package kr.touroot.global.config; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; diff --git a/backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java b/backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java similarity index 84% rename from backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java rename to backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java index 39b885c2..a29f0e30 100644 --- a/backend/src/main/java/woowacourse/touroot/global/config/WebMvcConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java @@ -1,12 +1,11 @@ -package woowacourse.touroot.global.config; +package kr.touroot.global.config; +import java.util.List; +import kr.touroot.global.auth.MemberAuthMethodArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import woowacourse.touroot.global.auth.MemberAuthMethodArgumentResolver; - -import java.util.List; @RequiredArgsConstructor @Configuration diff --git a/backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java b/backend/src/main/java/kr/touroot/global/entity/BaseEntity.java similarity index 94% rename from backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java rename to backend/src/main/java/kr/touroot/global/entity/BaseEntity.java index bb3527a6..8a57dc9a 100644 --- a/backend/src/main/java/woowacourse/touroot/entity/BaseEntity.java +++ b/backend/src/main/java/kr/touroot/global/entity/BaseEntity.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.entity; +package kr.touroot.global.entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java b/backend/src/main/java/kr/touroot/global/exception/BadRequestException.java similarity index 75% rename from backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java rename to backend/src/main/java/kr/touroot/global/exception/BadRequestException.java index 94306533..5309328e 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/BadRequestException.java +++ b/backend/src/main/java/kr/touroot/global/exception/BadRequestException.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.exception; +package kr.touroot.global.exception; public class BadRequestException extends RuntimeException { diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java b/backend/src/main/java/kr/touroot/global/exception/ClientException.java similarity index 74% rename from backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java rename to backend/src/main/java/kr/touroot/global/exception/ClientException.java index 5abf4cd8..b5c5d920 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/ClientException.java +++ b/backend/src/main/java/kr/touroot/global/exception/ClientException.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.exception; +package kr.touroot.global.exception; public class ClientException extends RuntimeException { diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java similarity index 95% rename from backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java rename to backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index 6b3c2d7b..b4525cf1 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -1,12 +1,12 @@ -package woowacourse.touroot.global.exception; +package kr.touroot.global.exception; +import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MaxUploadSizeExceededException; -import woowacourse.touroot.global.exception.dto.ExceptionResponse; @Slf4j @RestControllerAdvice diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/UnauthorizedException.java b/backend/src/main/java/kr/touroot/global/exception/UnauthorizedException.java similarity index 100% rename from backend/src/main/java/woowacourse/touroot/global/exception/UnauthorizedException.java rename to backend/src/main/java/kr/touroot/global/exception/UnauthorizedException.java diff --git a/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java b/backend/src/main/java/kr/touroot/global/exception/dto/ExceptionResponse.java similarity index 51% rename from backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java rename to backend/src/main/java/kr/touroot/global/exception/dto/ExceptionResponse.java index 8418e1c5..f758da9e 100644 --- a/backend/src/main/java/woowacourse/touroot/global/exception/dto/ExceptionResponse.java +++ b/backend/src/main/java/kr/touroot/global/exception/dto/ExceptionResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.exception.dto; +package kr.touroot.global.exception.dto; public record ExceptionResponse(String message) { } diff --git a/backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java b/backend/src/main/java/kr/touroot/image/controller/ImageController.java similarity index 83% rename from backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java rename to backend/src/main/java/kr/touroot/image/controller/ImageController.java index b14c406c..1348ae8f 100644 --- a/backend/src/main/java/woowacourse/touroot/image/controller/ImageController.java +++ b/backend/src/main/java/kr/touroot/image/controller/ImageController.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.image.controller; +package kr.touroot.image.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -7,6 +7,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; +import kr.touroot.global.exception.dto.ExceptionResponse; +import kr.touroot.image.service.ImageService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -14,8 +16,6 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import woowacourse.touroot.global.exception.dto.ExceptionResponse; -import woowacourse.touroot.image.service.ImageService; @Tag(name = "이미지") @RequiredArgsConstructor @@ -27,6 +27,10 @@ public class ImageController { @Operation(summary = "이미지 업로드") @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), @ApiResponse( responseCode = "400", description = "jpg, jpeg, png, webp가 아닌 확장자의 파일을 업로드할 때", diff --git a/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java b/backend/src/main/java/kr/touroot/image/domain/ImageFile.java similarity index 53% rename from backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java rename to backend/src/main/java/kr/touroot/image/domain/ImageFile.java index 00705be3..8f746bfc 100644 --- a/backend/src/main/java/woowacourse/touroot/image/domain/ImageFile.java +++ b/backend/src/main/java/kr/touroot/image/domain/ImageFile.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.image.domain; +package kr.touroot.image.domain; import java.util.List; +import kr.touroot.global.exception.BadRequestException; import lombok.Getter; import org.springframework.web.multipart.MultipartFile; -import woowacourse.touroot.global.exception.BadRequestException; @Getter public class ImageFile { @@ -13,30 +13,37 @@ public class ImageFile { private final MultipartFile file; public ImageFile(MultipartFile file) { - validateImageNames(file); + validate(file); this.file = file; } - private void validateImageNames(MultipartFile file) { + private void validate(MultipartFile file) { + validateNotNull(file); String fileName = file.getOriginalFilename(); - validateNotNull(fileName); + validateFileNameNotBlank(fileName); validateExtension(fileName); } - private void validateNotNull(String fileName) { - if (fileName == null) { - throw new BadRequestException("파일 형식이 잘못되었습니다."); + private void validateNotNull(MultipartFile file) { + if (file == null) { + throw new BadRequestException("파일을 전달 받지 못했습니다"); + } + } + + private void validateFileNameNotBlank(String fileName) { + if (fileName == null || fileName.isBlank()) { + throw new BadRequestException("파일 이름은 비어있을 수 없습니다"); } } public void validateExtension(String fileName) { int extensionIndex = fileName.lastIndexOf("."); if (extensionIndex == -1 || fileName.endsWith(".")) { - throw new BadRequestException("파일 형식이 잘못되었습니다."); + throw new BadRequestException("파일 형식이 잘못되었습니다"); } String extension = fileName.substring(extensionIndex + 1); if (!WHITE_LIST.contains(extension.toLowerCase())) { - throw new BadRequestException("파일 형식이 잘못되었습니다."); + throw new BadRequestException("지원하지 않는 확장자입니다: " + extension); } } } diff --git a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java similarity index 94% rename from backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java rename to backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java index 336cf86a..98b8345f 100644 --- a/backend/src/main/java/woowacourse/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.image.infrastructure; +package kr.touroot.image.infrastructure; import java.io.IOException; import java.util.ArrayList; @@ -12,8 +12,8 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.image.domain.ImageFile; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.domain.ImageFile; @Component public class AwsS3Provider { diff --git a/backend/src/main/java/woowacourse/touroot/image/service/ImageService.java b/backend/src/main/java/kr/touroot/image/service/ImageService.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/image/service/ImageService.java rename to backend/src/main/java/kr/touroot/image/service/ImageService.java index 587c5a9e..eee12c56 100644 --- a/backend/src/main/java/woowacourse/touroot/image/service/ImageService.java +++ b/backend/src/main/java/kr/touroot/image/service/ImageService.java @@ -1,11 +1,11 @@ -package woowacourse.touroot.image.service; +package kr.touroot.image.service; import java.util.List; +import kr.touroot.image.domain.ImageFile; +import kr.touroot.image.infrastructure.AwsS3Provider; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import woowacourse.touroot.image.domain.ImageFile; -import woowacourse.touroot.image.infrastructure.AwsS3Provider; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java new file mode 100644 index 00000000..8f9384f0 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -0,0 +1,71 @@ +package kr.touroot.member.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class Member extends BaseEntity { + + public static final int NICKNAME_MIN_LENGTH = 1; + public static final int NICKNAME_MAX_LENGTH = 20; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long kakaoId; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private String profileImageUri; + + public Member(Long id, Long kakaoId, String nickname, String profileImageUri) { + validate(kakaoId, nickname, profileImageUri); + this.id = id; + this.kakaoId = kakaoId; + this.nickname = nickname; + this.profileImageUri = profileImageUri; + } + + public Member(Long kakaoId, String nickname, String profileImageUri) { + this(null, kakaoId, nickname, profileImageUri); + } + + private void validate(Long kakaoId, String nickname, String profileImageUri) { + validateNotNull(kakaoId, nickname, profileImageUri); + validateNotBlank(nickname, profileImageUri); + validateNicknameLength(nickname); + } + + private void validateNotNull(Long kakaoId, String nickname, String profileImageUri) { + if (kakaoId == null || nickname == null || profileImageUri == null) { + throw new BadRequestException("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + } + + private void validateNotBlank(String nickname, String profileImageUri) { + if (nickname.isBlank() || profileImageUri.isBlank()) { + throw new BadRequestException("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + } + + private void validateNicknameLength(String nickname) { + if (NICKNAME_MIN_LENGTH > nickname.length() || nickname.length() > NICKNAME_MAX_LENGTH) { + throw new BadRequestException( + "닉네임은 " + NICKNAME_MIN_LENGTH + "자 이상, " + NICKNAME_MAX_LENGTH + "자 이하여야 합니다" + ); + } + } +} diff --git a/backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java b/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java similarity index 69% rename from backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java rename to backend/src/main/java/kr/touroot/member/repository/MemberRepository.java index fe748797..4ba5158d 100644 --- a/backend/src/main/java/woowacourse/touroot/member/repository/MemberRepository.java +++ b/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java @@ -1,8 +1,8 @@ -package woowacourse.touroot.member.repository; +package kr.touroot.member.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.member.domain.Member; +import kr.touroot.member.domain.Member; public interface MemberRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java b/backend/src/main/java/kr/touroot/place/domain/Place.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/place/domain/Place.java rename to backend/src/main/java/kr/touroot/place/domain/Place.java index 555d74cc..c0053214 100644 --- a/backend/src/main/java/woowacourse/touroot/place/domain/Place.java +++ b/backend/src/main/java/kr/touroot/place/domain/Place.java @@ -1,11 +1,15 @@ -package woowacourse.touroot.place.domain; - -import jakarta.persistence.*; +package kr.touroot.place.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import kr.touroot.global.entity.BaseEntity; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -31,7 +35,7 @@ public class Place extends BaseEntity { public Place(String name, String latitude, String longitude, String googlePlaceId) { this(null, name, latitude, longitude, googlePlaceId); } - + public Place(String name, String latitude, String longitude) { this(null, name, latitude, longitude, null); } diff --git a/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java b/backend/src/main/java/kr/touroot/place/repository/PlaceRepository.java similarity index 73% rename from backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java rename to backend/src/main/java/kr/touroot/place/repository/PlaceRepository.java index 4b0c8ce6..85881925 100644 --- a/backend/src/main/java/woowacourse/touroot/place/repository/PlaceRepository.java +++ b/backend/src/main/java/kr/touroot/place/repository/PlaceRepository.java @@ -1,9 +1,8 @@ -package woowacourse.touroot.place.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.place.domain.Place; +package kr.touroot.place.repository; import java.util.Optional; +import kr.touroot.place.domain.Place; +import org.springframework.data.jpa.repository.JpaRepository; public interface PlaceRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java similarity index 89% rename from backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java rename to backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index 0f689ee0..91c7dc70 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.controller; +package kr.touroot.travelogue.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -9,6 +9,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.net.URI; +import kr.touroot.global.exception.dto.ExceptionResponse; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.service.TravelogueFacadeService; import lombok.RequiredArgsConstructor; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -22,10 +26,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import woowacourse.touroot.global.exception.dto.ExceptionResponse; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; -import woowacourse.touroot.travelogue.service.TravelogueFacadeService; @Tag(name = "여행기") @RequiredArgsConstructor @@ -35,7 +35,6 @@ public class TravelogueController { private final TravelogueFacadeService travelogueFacadeService; - @Operation(summary = "여행기 작성") @ApiResponses(value = { @ApiResponse( @@ -69,7 +68,7 @@ public ResponseEntity createTravelogue(@Valid @RequestBody T ), }) @GetMapping("/{id}") - public ResponseEntity findTravelogue(@Valid @PathVariable Long id) { + public ResponseEntity findTravelogue(@PathVariable Long id) { return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id)); } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java similarity index 90% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java rename to backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index 18856a22..0743e7c4 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -1,16 +1,16 @@ -package woowacourse.touroot.travelogue.domain; +package kr.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import kr.touroot.global.entity.BaseEntity; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; @Getter @EqualsAndHashCode(of = "id", callSuper = false) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TravelogueDay.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java similarity index 91% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/TravelogueDay.java rename to backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java index 9d15e83a..e8d72aea 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TravelogueDay.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.domain; +package kr.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,12 +8,12 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import kr.touroot.global.entity.BaseEntity; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; @Getter @EqualsAndHashCode(of = "id", callSuper = false) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java similarity index 92% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java rename to backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java index 31b71b9d..0851dd4e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePhoto.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.domain; +package kr.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,12 +8,12 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import kr.touroot.global.entity.BaseEntity; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; @Getter @EqualsAndHashCode(of = "id", callSuper = false) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java similarity index 91% rename from backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java rename to backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index 670abd98..3c6635e4 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.domain; +package kr.touroot.travelogue.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -13,8 +13,8 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.place.domain.Place; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.place.domain.Place; @Getter @EqualsAndHashCode(of = "id", callSuper = false) diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java rename to backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java index 9a84a0ed..da5737e0 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueDayRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java @@ -1,11 +1,11 @@ -package woowacourse.touroot.travelogue.dto.request; +package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; public record TravelogueDayRequest( @Schema(description = "여행기 장소 목록") diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java similarity index 74% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java rename to backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java index e5c25ac7..68f1fc26 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePhotoRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.dto.request; +package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; public record TraveloguePhotoRequest( @Schema(description = "여행기 장소 사진 URL", example = "photo.png") diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java similarity index 85% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java rename to backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index 9ded0184..88997afa 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -1,12 +1,12 @@ -package woowacourse.touroot.travelogue.dto.request; +package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.place.domain.Place; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePlace; public record TraveloguePlaceRequest( @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePositionRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java similarity index 90% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePositionRequest.java rename to backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java index a71d690b..aa7ee56e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TraveloguePositionRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelogue.dto.request; +package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java similarity index 88% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java rename to backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 1450b8c8..fb59d22a 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -1,10 +1,10 @@ -package woowacourse.touroot.travelogue.dto.request; +package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; -import woowacourse.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.Travelogue; public record TravelogueRequest( @Schema(description = "여행기 제목", example = "서울 강남 여행기") diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueDayResponse.java similarity index 83% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java rename to backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueDayResponse.java index bc81a312..b3a075f0 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueDayResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueDayResponse.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.dto.response; +package kr.touroot.travelogue.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; +import kr.touroot.travelogue.domain.TravelogueDay; import lombok.Builder; -import woowacourse.touroot.travelogue.domain.TravelogueDay; @Builder public record TravelogueDayResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java similarity index 89% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java rename to backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java index fc6b92a9..d7b62ed1 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePlaceResponse.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.dto.response; +package kr.touroot.travelogue.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import lombok.Builder; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.domain.TraveloguePlace; @Builder public record TraveloguePlaceResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePositionResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java similarity index 83% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePositionResponse.java rename to backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java index dc26d2c6..36051242 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TraveloguePositionResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java @@ -1,8 +1,8 @@ -package woowacourse.touroot.travelogue.dto.response; +package kr.touroot.travelogue.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import kr.touroot.travelogue.domain.TraveloguePlace; import lombok.Builder; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; @Builder public record TraveloguePositionResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java similarity index 92% rename from backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueResponse.java rename to backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index cfb554a4..07127a59 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -1,11 +1,11 @@ -package woowacourse.touroot.travelogue.dto.response; +package kr.touroot.travelogue.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; import lombok.Builder; -import woowacourse.touroot.travelogue.domain.Travelogue; @Builder public record TravelogueResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueDayRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java similarity index 58% rename from backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueDayRepository.java rename to backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java index b1fe0a89..ef5f2f46 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueDayRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.repository; +package kr.touroot.travelogue.repository; import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; public interface TravelogueDayRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePhotoRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java similarity index 59% rename from backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePhotoRepository.java rename to backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java index 90f508a2..cabb5f95 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.repository; +package kr.touroot.travelogue.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; public interface TraveloguePhotoRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePlaceRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java similarity index 59% rename from backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePlaceRepository.java rename to backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java index f28af54d..2eb48f46 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TraveloguePlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.repository; +package kr.touroot.travelogue.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePlace; public interface TraveloguePlaceRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java similarity index 57% rename from backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java rename to backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java index 0b424122..5e17ae01 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/repository/TravelogueRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java @@ -1,7 +1,7 @@ -package woowacourse.touroot.travelogue.repository; +package kr.touroot.travelogue.repository; +import kr.touroot.travelogue.domain.Travelogue; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelogue.domain.Travelogue; public interface TravelogueRepository extends JpaRepository { } diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java similarity index 74% rename from backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java rename to backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java index 3e58305a..38ac5532 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueDayService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java @@ -1,16 +1,16 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.repository.TravelogueDayRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java similarity index 82% rename from backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java rename to backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index a1db74a5..ab3809a6 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -1,25 +1,25 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; import java.util.Comparator; import java.util.List; import java.util.Map; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.response.TravelogueDayResponse; +import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import kr.touroot.travelogue.dto.response.TravelogueResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; -import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; -import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java similarity index 78% rename from backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java rename to backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java index 8fbe27df..1ae68436 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java @@ -1,14 +1,14 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.repository.TraveloguePhotoRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java similarity index 75% rename from backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java rename to backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java index fa5fb07e..e2442f2d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java @@ -1,18 +1,18 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; +import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.repository.TraveloguePlaceRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.place.repository.PlaceRepository; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java similarity index 72% rename from backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java rename to backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index cfeb160c..c519a808 100644 --- a/backend/src/main/java/woowacourse/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -1,13 +1,13 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.repository.TravelogueRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.repository.TravelogueRepository; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java similarity index 74% rename from backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java rename to backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java index 9b5ad32a..b73d7d89 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.controller; +package kr.touroot.travelplan.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -7,14 +7,19 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import kr.touroot.global.exception.dto.ExceptionResponse; +import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import woowacourse.touroot.global.exception.dto.ExceptionResponse; -import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; -import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; -import woowacourse.touroot.travelplan.service.TravelPlanService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @Tag(name = "여행 계획") @RequiredArgsConstructor diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java similarity index 71% rename from backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java rename to backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index b0c9def1..40c5807d 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -1,14 +1,17 @@ -package woowacourse.touroot.travelplan.domain; +package kr.touroot.travelplan.domain; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDate; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.global.exception.BadRequestException; - -import java.time.LocalDate; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java similarity index 68% rename from backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java rename to backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java index 4f417f14..fcfaa349 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanDay.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java @@ -1,13 +1,19 @@ -package woowacourse.touroot.travelplan.domain; - -import jakarta.persistence.*; +package kr.touroot.travelplan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import kr.touroot.global.entity.BaseEntity; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; - -import java.time.LocalDate; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java similarity index 67% rename from backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java rename to backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index 71733dad..d69b4d4f 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -1,12 +1,19 @@ -package woowacourse.touroot.travelplan.domain; - -import jakarta.persistence.*; +package kr.touroot.travelplan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; -import woowacourse.touroot.place.domain.Place; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.place.domain.Place; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayCreateRequest.java similarity index 79% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayCreateRequest.java index e58229f7..4fbcc91b 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanDayCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayCreateRequest.java @@ -1,13 +1,12 @@ -package woowacourse.touroot.travelplan.dto.request; +package kr.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import woowacourse.touroot.travelplan.domain.TravelPlan; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; - import java.util.List; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.domain.TravelPlanDay; public record PlanDayCreateRequest( @Schema(description = "여행 장소 정보") diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java similarity index 81% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java index 0647fd4e..718e2e67 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java @@ -1,13 +1,13 @@ -package woowacourse.touroot.travelplan.dto.request; +package kr.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import kr.touroot.place.domain.Place; +import kr.touroot.travelplan.domain.TravelPlanDay; +import kr.touroot.travelplan.domain.TravelPlanPlace; import lombok.Builder; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; -import woowacourse.touroot.travelplan.domain.TravelPlanPlace; @Builder public record PlanPlaceCreateRequest( diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPositionCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionCreateRequest.java similarity index 90% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPositionCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionCreateRequest.java index fd319ac3..f6ccaf1e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/PlanPositionCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionCreateRequest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto.request; +package kr.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java similarity index 91% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java index bf986bc9..b8a844a8 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/request/TravelPlanCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java @@ -1,15 +1,14 @@ -package woowacourse.touroot.travelplan.dto.request; +package kr.touroot.travelplan.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import lombok.Builder; -import woowacourse.touroot.travelplan.domain.TravelPlan; - import java.time.LocalDate; import java.util.List; +import kr.touroot.travelplan.domain.TravelPlan; +import lombok.Builder; @Builder public record TravelPlanCreateRequest( diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java similarity index 76% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java index 66c1cd08..0962f2fe 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanCreateResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.travelplan.dto.response; +package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java similarity index 84% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java index 62493e38..e1e1acd3 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanDayResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java @@ -1,8 +1,8 @@ -package woowacourse.touroot.travelplan.dto.response; +package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; +import kr.touroot.travelplan.domain.TravelPlanDay; import java.time.LocalDate; import java.util.List; diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java similarity index 83% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java index e71665e2..c5d2f540 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelplan.dto.response; +package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +import kr.touroot.place.domain.Place; +import kr.touroot.travelplan.domain.TravelPlanPlace; @Builder public record TravelPlanPlaceResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPositionResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java similarity index 83% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPositionResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java index d5712ba0..349d557e 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanPositionResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java @@ -1,8 +1,8 @@ -package woowacourse.touroot.travelplan.dto.response; +package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import kr.touroot.place.domain.Place; import lombok.Builder; -import woowacourse.touroot.place.domain.Place; @Builder public record TravelPlanPositionResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java similarity index 88% rename from backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java index ecc6d580..fac07330 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/dto/response/TravelPlanResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java @@ -1,11 +1,10 @@ -package woowacourse.touroot.travelplan.dto.response; +package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; -import woowacourse.touroot.travelplan.domain.TravelPlan; - import java.time.LocalDate; import java.util.List; +import kr.touroot.travelplan.domain.TravelPlan; +import lombok.Builder; @Builder public record TravelPlanResponse( diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java similarity index 58% rename from backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java rename to backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java index 4c576c57..f5e0d0e2 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanDayRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java @@ -1,10 +1,9 @@ -package woowacourse.touroot.travelplan.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelplan.domain.TravelPlan; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; +package kr.touroot.travelplan.repository; import java.util.List; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.domain.TravelPlanDay; +import org.springframework.data.jpa.repository.JpaRepository; public interface TravelPlanDayRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java similarity index 57% rename from backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java rename to backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java index d969112b..c7b5bca3 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanPlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java @@ -1,10 +1,9 @@ -package woowacourse.touroot.travelplan.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; -import woowacourse.touroot.travelplan.domain.TravelPlanPlace; +package kr.touroot.travelplan.repository; import java.util.List; +import kr.touroot.travelplan.domain.TravelPlanDay; +import kr.touroot.travelplan.domain.TravelPlanPlace; +import org.springframework.data.jpa.repository.JpaRepository; public interface TravelPlanPlaceRepository extends JpaRepository { diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java similarity index 57% rename from backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java rename to backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java index 0665c7a6..0489b9fa 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/repository/TravelPlanRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java @@ -1,7 +1,7 @@ -package woowacourse.touroot.travelplan.repository; +package kr.touroot.travelplan.repository; +import kr.touroot.travelplan.domain.TravelPlan; import org.springframework.data.jpa.repository.JpaRepository; -import woowacourse.touroot.travelplan.domain.TravelPlan; public interface TravelPlanRepository extends JpaRepository { } diff --git a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java similarity index 74% rename from backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java rename to backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index 2027680b..ba057ce1 100644 --- a/backend/src/main/java/woowacourse/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -1,27 +1,26 @@ -package woowacourse.touroot.travelplan.service; +package kr.touroot.travelplan.service; +import java.util.Comparator; +import java.util.List; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; +import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.domain.TravelPlanDay; +import kr.touroot.travelplan.domain.TravelPlanPlace; +import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; +import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import kr.touroot.travelplan.dto.response.TravelPlanDayResponse; +import kr.touroot.travelplan.dto.response.TravelPlanPlaceResponse; +import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.repository.TravelPlanDayRepository; +import kr.touroot.travelplan.repository.TravelPlanPlaceRepository; +import kr.touroot.travelplan.repository.TravelPlanRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.place.repository.PlaceRepository; -import woowacourse.touroot.travelplan.domain.TravelPlan; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; -import woowacourse.touroot.travelplan.domain.TravelPlanPlace; -import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; -import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; -import woowacourse.touroot.travelplan.dto.response.TravelPlanDayResponse; -import woowacourse.touroot.travelplan.dto.response.TravelPlanPlaceResponse; -import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; -import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; -import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; -import woowacourse.touroot.travelplan.repository.TravelPlanRepository; - -import java.util.Comparator; -import java.util.List; @RequiredArgsConstructor @Service diff --git a/backend/src/main/java/woowacourse/touroot/member/domain/Member.java b/backend/src/main/java/woowacourse/touroot/member/domain/Member.java deleted file mode 100644 index 3aac33ec..00000000 --- a/backend/src/main/java/woowacourse/touroot/member/domain/Member.java +++ /dev/null @@ -1,36 +0,0 @@ -package woowacourse.touroot.member.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import woowacourse.touroot.entity.BaseEntity; - -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PUBLIC) -@Getter -@Entity -public class Member extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private Long kakaoId; - - @Column(nullable = false) - private String nickname; - - @Column(nullable = false) - private String profileImageUri; - - public Member(Long kakaoId, String nickname, String profileImageUri) { - this(null, kakaoId, nickname, profileImageUri); - } -} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java similarity index 84% rename from backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java rename to backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java index 6faaa671..f04d2e2e 100644 --- a/backend/src/test/java/woowacourse/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java @@ -1,24 +1,23 @@ -package woowacourse.touroot.authentication.controller; +package kr.touroot.authentication.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Disabled; +import kr.touroot.authentication.dto.response.LoginResponse; +import kr.touroot.authentication.service.LoginService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import woowacourse.touroot.authentication.dto.response.LoginResponse; -import woowacourse.touroot.authentication.service.LoginService; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@Disabled @WebMvcTest(LoginController.class) class LoginControllerTest { @@ -28,6 +27,8 @@ class LoginControllerTest { private ObjectMapper objectMapper; @MockBean private LoginService loginService; + @MockBean + JpaMetamodelMappingContext jpaMetamodelMappingContext; @DisplayName("카카오 로그인 요청을 처리할 수 있다") @Test diff --git a/backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java similarity index 54% rename from backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java rename to backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java index 0a4e1778..f03a1c6b 100644 --- a/backend/src/test/java/woowacourse/touroot/authentication/fixture/MemberFixture.java +++ b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java @@ -1,6 +1,6 @@ -package woowacourse.touroot.authentication.fixture; +package kr.touroot.authentication.fixture; -import woowacourse.touroot.member.domain.Member; +import kr.touroot.member.domain.Member; public class MemberFixture { diff --git a/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java b/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java new file mode 100644 index 00000000..308f3758 --- /dev/null +++ b/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java @@ -0,0 +1,12 @@ +package kr.touroot.authentication.fixture; + +import kr.touroot.authentication.dto.response.OauthUserInformationResponse; +import kr.touroot.authentication.dto.response.kakao.KakaoAccount; +import kr.touroot.authentication.dto.response.kakao.KakaoProfile; + +public class OauthUserInformationFixture { + + public static final OauthUserInformationResponse USER_1_OAUTH_INFORMATION = new OauthUserInformationResponse( + 1L, new KakaoAccount(new KakaoProfile("리비", "img-url")) + ); +} diff --git a/backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java b/backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java similarity index 92% rename from backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java rename to backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java index 530f0fb7..6f267dd7 100644 --- a/backend/src/test/java/woowacourse/touroot/authentication/infrastructure/JwtTokenProviderTest.java +++ b/backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.infrastructure; +package kr.touroot.authentication.infrastructure; import static org.assertj.core.api.Assertions.assertThat; diff --git a/backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java similarity index 82% rename from backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java rename to backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java index 397a1549..f3a3c256 100644 --- a/backend/src/test/java/woowacourse/touroot/authentication/service/LoginServiceTest.java +++ b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.authentication.service; +package kr.touroot.authentication.service; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -7,19 +7,19 @@ import static org.mockito.Mockito.when; import java.util.Optional; +import kr.touroot.authentication.dto.response.LoginResponse; +import kr.touroot.authentication.fixture.MemberFixture; +import kr.touroot.authentication.fixture.OauthUserInformationFixture; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.authentication.infrastructure.KakaoOauthProvider; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import woowacourse.touroot.authentication.dto.response.LoginResponse; -import woowacourse.touroot.authentication.fixture.MemberFixture; -import woowacourse.touroot.authentication.fixture.OauthUserInformationFixture; -import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; -import woowacourse.touroot.authentication.infrastructure.KakaoOauthProvider; -import woowacourse.touroot.member.domain.Member; -import woowacourse.touroot.member.repository.MemberRepository; @DisplayName("로그인 서비스") @ExtendWith(MockitoExtension.class) diff --git a/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java b/backend/src/test/java/kr/touroot/global/AcceptanceTest.java similarity index 93% rename from backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java rename to backend/src/test/java/kr/touroot/global/AcceptanceTest.java index d40fb01b..248d0a37 100644 --- a/backend/src/test/java/woowacourse/touroot/global/AcceptanceTest.java +++ b/backend/src/test/java/kr/touroot/global/AcceptanceTest.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global; +package kr.touroot.global; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java b/backend/src/test/java/kr/touroot/global/ServiceTest.java similarity index 88% rename from backend/src/test/java/woowacourse/touroot/global/ServiceTest.java rename to backend/src/test/java/kr/touroot/global/ServiceTest.java index 77d95235..8422e47b 100644 --- a/backend/src/test/java/woowacourse/touroot/global/ServiceTest.java +++ b/backend/src/test/java/kr/touroot/global/ServiceTest.java @@ -1,14 +1,13 @@ -package woowacourse.touroot.global; +package kr.touroot.global; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import kr.touroot.utils.DatabaseCleaner; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import woowacourse.touroot.utils.DatabaseCleaner; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; @DataJpaTest @Transactional(propagation = Propagation.NOT_SUPPORTED) diff --git a/backend/src/test/java/kr/touroot/image/domain/ImageFileTest.java b/backend/src/test/java/kr/touroot/image/domain/ImageFileTest.java new file mode 100644 index 00000000..d0ed406a --- /dev/null +++ b/backend/src/test/java/kr/touroot/image/domain/ImageFileTest.java @@ -0,0 +1,69 @@ +package kr.touroot.image.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +@DisplayName("이미지 파일") +class ImageFileTest { + + private MultipartFile validFile; + private MultipartFile nullFile; + private MultipartFile originalFileNameNullFile; + private MultipartFile notSupportingFile; + private MultipartFile fileWithNoExtension; + + @BeforeEach + void setUp() { + validFile = new MockMultipartFile("file", "image.jpg", "image/jpeg", "image content".getBytes()); + nullFile = null; + originalFileNameNullFile = new MockMultipartFile("file", null, "text/plain", "image content".getBytes()); + notSupportingFile = new MockMultipartFile("file", "image.txt", "text/plain", "image content".getBytes()); + fileWithNoExtension = new MockMultipartFile("file", "image.", "text/plain", "image content".getBytes()); + } + + @DisplayName("유효한 파일로 이미지 파일 도메인을 생성할 수 있다") + @Test + void createImageFileWithValidFile() { + assertThatCode(() -> new ImageFile(validFile)) + .doesNotThrowAnyException(); + } + + @DisplayName("파일이 null이면 이미지 파일 생성 시에 예외가 발생한다") + @Test + void createImageFileWithNullFile() { + assertThatThrownBy(() -> new ImageFile(nullFile)) + .isInstanceOf(BadRequestException.class) + .hasMessage("파일을 전달 받지 못했습니다"); + } + + @DisplayName("파일 이름이 비어있는 경우 이미지 파일 생성 시에 예외가 발생한다") + @Test + void createImageFileWithOriginalFileNameNullFile() { + assertThatThrownBy(() -> new ImageFile(originalFileNameNullFile)) + .isInstanceOf(BadRequestException.class) + .hasMessage("파일 이름은 비어있을 수 없습니다"); + } + + @DisplayName("지원하지 않는 파일 확장자의 이미지 파일을 생성 시에 에외가 발생한다") + @Test + void createImageFileWithNotValidExtensionFile() { + assertThatThrownBy(() -> new ImageFile(notSupportingFile)) + .isInstanceOf(BadRequestException.class) + .hasMessage("지원하지 않는 확장자입니다: " + "txt"); + } + + @DisplayName("파일 이름 형식이 올바르지 않은 경우 이미지 파일 생성 시에 예외가 발생한다") + @Test + void createImageFileWithInvalidFormFile() { + assertThatThrownBy(() -> new ImageFile(fileWithNoExtension)) + .isInstanceOf(BadRequestException.class) + .hasMessage("파일 형식이 잘못되었습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java new file mode 100644 index 00000000..850e20b7 --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java @@ -0,0 +1,77 @@ +package kr.touroot.member.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("멤버") +class MemberTest { + + private static final Long VALID_SOCIAl_ID = 1L; + private static final String VALID_NICKNAME = "nickname"; + private static final String VALID_PROFILE_IMAGE_URL = "url"; + + + @DisplayName("검증 규칙을 통과하는 멤버 생성은 예외가 발생하지 않는다") + @Test + void createMemberWithValidData() { + assertThatCode(() -> new Member(1L, VALID_NICKNAME, "url")) + .doesNotThrowAnyException(); + } + + @DisplayName("카카오 아이디가 null일 때 멤버 생성 시 예외가 발생한다") + @Test + void createMemberWithKakaoIdNull() { + assertThatThrownBy(() -> new Member(null, VALID_NICKNAME, "url")) + .isInstanceOf(BadRequestException.class) + .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + + @DisplayName("닉네임이 null인 경우 멤버 생성 시 예외가 발생한다") + @Test + void createMemberWithNicknameNull() { + assertThatThrownBy(() -> new Member(1L, null, "url")) + .isInstanceOf(BadRequestException.class) + .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + + @DisplayName("프로필 이미지 경로가 null일 경우 멤버 생성 시 예외가 발생한다") + @Test + void createMemberWithProfileImageUrlNull() { + assertThatThrownBy(() -> new Member(1L, VALID_NICKNAME, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + + @DisplayName("닉네임이 비어 있는 경우 멤버 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void createMemberWithBlankNickname(String blankNickname) { + assertThatThrownBy(() -> new Member(1L, blankNickname, "url")) + .isInstanceOf(BadRequestException.class) + .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + + @DisplayName("프로필 이미지 경로가 비어 있는 경우 멤버 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void createMemberWithProfileImageBlank(String blankUrl) { + assertThatThrownBy(() -> new Member(1L, VALID_NICKNAME, blankUrl)) + .isInstanceOf(BadRequestException.class) + .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + } + + @DisplayName("닉네임의 길이가 범위를 벗어나면 멤버 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"21-length-nicknameeee", "22-length-nicknameeeee"}) + void createMemberWithInvalidLengthNickname(String invalidLengthNickname) { + assertThatThrownBy(() -> new Member(1L, invalidLengthNickname, "url")) + .isInstanceOf(BadRequestException.class) + .hasMessage("닉네임은 1자 이상, 20자 이하여야 합니다"); + } +} diff --git a/backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java b/backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java similarity index 86% rename from backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java rename to backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java index 2cf69db5..b0b46f17 100644 --- a/backend/src/test/java/woowacourse/touroot/place/fixture/PlaceFixture.java +++ b/backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java @@ -1,6 +1,6 @@ -package woowacourse.touroot.place.fixture; +package kr.touroot.place.fixture; -import woowacourse.touroot.place.domain.Place; +import kr.touroot.place.domain.Place; public enum PlaceFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java similarity index 85% rename from backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java rename to backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index cdaf34a9..610e3a50 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -1,9 +1,20 @@ -package woowacourse.touroot.travelogue.controller; +package kr.touroot.travelogue.controller; + +import static org.hamcrest.Matchers.is; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.member.domain.Member; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.fixture.TravelogueResponseFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -11,17 +22,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.data.domain.Page; import org.springframework.http.HttpHeaders; -import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; -import woowacourse.touroot.global.AcceptanceTest; -import woowacourse.touroot.member.domain.Member; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; -import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; -import woowacourse.touroot.travelogue.fixture.TravelogueResponseFixture; -import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; - -import static org.hamcrest.Matchers.is; @DisplayName("여행기 컨트롤러") @AcceptanceTest diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java similarity index 67% rename from backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java rename to backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java index 97449d60..768d15ef 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueDayFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.fixture; +package kr.touroot.travelogue.fixture; -import static woowacourse.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; +import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; public enum TravelogueDayFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java similarity index 79% rename from backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java rename to backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java index b5ad9e8e..5de3f0b6 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java @@ -1,6 +1,6 @@ -package woowacourse.touroot.travelogue.fixture; +package kr.touroot.travelogue.fixture; -import woowacourse.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.Travelogue; public enum TravelogueFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java similarity index 69% rename from backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java rename to backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java index 0667011a..e9e43d09 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePhotoFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java @@ -1,9 +1,9 @@ -package woowacourse.touroot.travelogue.fixture; +package kr.touroot.travelogue.fixture; -import static woowacourse.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; +import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; public enum TraveloguePhotoFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java similarity index 66% rename from backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java rename to backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java index c63a28f0..23d28a88 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TraveloguePlaceFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java @@ -1,11 +1,11 @@ -package woowacourse.touroot.travelogue.fixture; +package kr.touroot.travelogue.fixture; -import static woowacourse.touroot.place.fixture.PlaceFixture.PLACE; -import static woowacourse.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; +import static kr.touroot.place.fixture.PlaceFixture.PLACE; +import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.place.domain.Place; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePlace; public enum TraveloguePlaceFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java similarity index 74% rename from backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java rename to backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index c5f64778..ffe53d0d 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -1,12 +1,12 @@ -package woowacourse.touroot.travelogue.fixture; +package kr.touroot.travelogue.fixture; import java.util.List; import org.springframework.stereotype.Component; -import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePositionRequest; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.dto.request.TraveloguePositionRequest; +import kr.touroot.travelogue.dto.request.TravelogueRequest; @Component public class TravelogueRequestFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java similarity index 85% rename from backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java rename to backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index f8a36b23..3334e397 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -1,13 +1,13 @@ -package woowacourse.touroot.travelogue.fixture; +package kr.touroot.travelogue.fixture; import java.util.List; +import kr.touroot.travelogue.dto.response.TravelogueDayResponse; +import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import kr.touroot.travelogue.dto.response.TraveloguePositionResponse; +import kr.touroot.travelogue.dto.response.TravelogueResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.stereotype.Component; -import woowacourse.touroot.travelogue.dto.response.TravelogueDayResponse; -import woowacourse.touroot.travelogue.dto.response.TraveloguePlaceResponse; -import woowacourse.touroot.travelogue.dto.response.TraveloguePositionResponse; -import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; @Component public class TravelogueResponseFixture { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java similarity index 68% rename from backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java rename to backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 1c36e973..06d979a5 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,25 +1,25 @@ -package woowacourse.touroot.travelogue.helper; - +package kr.touroot.travelogue.helper; + +import static kr.touroot.place.fixture.PlaceFixture.PLACE; +import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; +import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; +import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; +import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; + +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; +import kr.touroot.place.domain.Place; +import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.repository.TravelogueDayRepository; +import kr.touroot.travelogue.repository.TraveloguePhotoRepository; +import kr.touroot.travelogue.repository.TraveloguePlaceRepository; +import kr.touroot.travelogue.repository.TravelogueRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import woowacourse.touroot.member.domain.Member; -import woowacourse.touroot.member.repository.MemberRepository; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.place.repository.PlaceRepository; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.repository.TravelogueDayRepository; -import woowacourse.touroot.travelogue.repository.TraveloguePhotoRepository; -import woowacourse.touroot.travelogue.repository.TraveloguePlaceRepository; -import woowacourse.touroot.travelogue.repository.TravelogueRepository; - -import static woowacourse.touroot.place.fixture.PlaceFixture.PLACE; -import static woowacourse.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; -import static woowacourse.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; -import static woowacourse.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; -import static woowacourse.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; @Component public class TravelogueTestHelper { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java similarity index 82% rename from backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java rename to backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java index 3dfa5e81..fae4b714 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -1,27 +1,26 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import java.util.Map; +import kr.touroot.global.ServiceTest; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; 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.context.annotation.Import; -import woowacourse.touroot.global.ServiceTest; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.dto.request.TravelogueDayRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; -import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @DisplayName("여행기 일자 서비스") @Import(value = {TravelogueDayService.class, TravelogueTestHelper.class}) diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java similarity index 82% rename from backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java rename to backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 4707a5f4..5f63ebe7 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -1,5 +1,14 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; +import static org.assertj.core.api.Assertions.assertThat; + +import kr.touroot.global.ServiceTest; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.fixture.TravelogueResponseFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -7,15 +16,6 @@ import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import woowacourse.touroot.global.ServiceTest; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.dto.response.TravelogueResponse; -import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; -import woowacourse.touroot.travelogue.fixture.TravelogueResponseFixture; -import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; - -import static org.assertj.core.api.Assertions.assertThat; @DisplayName("여행기 Facade 서비스") @Import(value = { diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java similarity index 77% rename from backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java rename to backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java index 7f8b5dd2..6c91f567 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -1,22 +1,21 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import kr.touroot.global.ServiceTest; +import kr.touroot.place.domain.Place; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePhoto; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import woowacourse.touroot.global.ServiceTest; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePhoto; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; -import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; @DisplayName("여행기 사진 서비스") @Import(value = {TraveloguePhotoService.class, TravelogueTestHelper.class}) diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java similarity index 81% rename from backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java rename to backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 3d241f14..833add72 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -1,21 +1,21 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; 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.context.annotation.Import; -import woowacourse.touroot.global.ServiceTest; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.domain.TravelogueDay; -import woowacourse.touroot.travelogue.domain.TraveloguePlace; -import woowacourse.touroot.travelogue.dto.request.TraveloguePhotoRequest; -import woowacourse.touroot.travelogue.dto.request.TraveloguePlaceRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; -import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; +import kr.touroot.global.ServiceTest; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; import java.util.List; import java.util.Map; diff --git a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java similarity index 84% rename from backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java rename to backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 2cdd50fc..6c47150a 100644 --- a/backend/src/test/java/woowacourse/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -1,23 +1,23 @@ -package woowacourse.touroot.travelogue.service; +package kr.touroot.travelogue.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import kr.touroot.global.ServiceTest; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; 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.context.annotation.Import; import org.springframework.data.domain.Pageable; -import woowacourse.touroot.global.ServiceTest; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelogue.domain.Travelogue; -import woowacourse.touroot.travelogue.dto.request.TravelogueRequest; -import woowacourse.touroot.travelogue.fixture.TravelogueRequestFixture; -import woowacourse.touroot.travelogue.helper.TravelogueTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @DisplayName("여행기 서비스") @Import(value = {TravelogueService.class, TravelogueTestHelper.class}) diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java similarity index 89% rename from backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java rename to backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index 94417828..e03c547f 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -1,27 +1,26 @@ -package woowacourse.touroot.travelplan.controller; +package kr.touroot.travelplan.controller; + +import static org.hamcrest.Matchers.is; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.time.LocalDate; +import java.util.List; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; +import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; +import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import kr.touroot.travelplan.helper.TravelPlanTestHelper; +import kr.touroot.utils.DatabaseCleaner; 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.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; -import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; -import woowacourse.touroot.global.AcceptanceTest; -import woowacourse.touroot.member.domain.Member; -import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanPositionCreateRequest; -import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.helper.TravelPlanTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; - -import java.time.LocalDate; -import java.util.List; - -import static org.hamcrest.Matchers.is; @DisplayName("여행 계획 컨트롤러") @AcceptanceTest diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java similarity index 85% rename from backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java rename to backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index 0548a6a6..5c86fb72 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -1,12 +1,11 @@ -package woowacourse.touroot.travelplan.domain; +package kr.touroot.travelplan.domain; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import woowacourse.touroot.global.exception.BadRequestException; +import static org.assertj.core.api.Assertions.assertThatCode; import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThatCode; +import kr.touroot.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("여행 계획") class TravelPlanTest { diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java similarity index 79% rename from backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java rename to backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 26242d35..47f1bd1e 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -1,19 +1,18 @@ -package woowacourse.touroot.travelplan.helper; +package kr.touroot.travelplan.helper; +import java.time.LocalDate; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; +import kr.touroot.place.domain.Place; +import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.domain.TravelPlanDay; +import kr.touroot.travelplan.domain.TravelPlanPlace; +import kr.touroot.travelplan.repository.TravelPlanDayRepository; +import kr.touroot.travelplan.repository.TravelPlanPlaceRepository; +import kr.touroot.travelplan.repository.TravelPlanRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import woowacourse.touroot.member.domain.Member; -import woowacourse.touroot.member.repository.MemberRepository; -import woowacourse.touroot.place.domain.Place; -import woowacourse.touroot.place.repository.PlaceRepository; -import woowacourse.touroot.travelplan.domain.TravelPlan; -import woowacourse.touroot.travelplan.domain.TravelPlanDay; -import woowacourse.touroot.travelplan.domain.TravelPlanPlace; -import woowacourse.touroot.travelplan.repository.TravelPlanDayRepository; -import woowacourse.touroot.travelplan.repository.TravelPlanPlaceRepository; -import woowacourse.touroot.travelplan.repository.TravelPlanRepository; - -import java.time.LocalDate; @Component public class TravelPlanTestHelper { diff --git a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java similarity index 85% rename from backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java rename to backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index f4ea0034..2b2e87e0 100644 --- a/backend/src/test/java/woowacourse/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -1,26 +1,25 @@ -package woowacourse.touroot.travelplan.service; +package kr.touroot.travelplan.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import java.util.List; +import kr.touroot.global.ServiceTest; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; +import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; +import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; +import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; +import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.helper.TravelPlanTestHelper; +import kr.touroot.utils.DatabaseCleaner; 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.context.annotation.Import; -import woowacourse.touroot.global.ServiceTest; -import woowacourse.touroot.global.exception.BadRequestException; -import woowacourse.touroot.travelplan.dto.request.PlanDayCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanPlaceCreateRequest; -import woowacourse.touroot.travelplan.dto.request.PlanPositionCreateRequest; -import woowacourse.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import woowacourse.touroot.travelplan.dto.response.TravelPlanCreateResponse; -import woowacourse.touroot.travelplan.dto.response.TravelPlanResponse; -import woowacourse.touroot.travelplan.helper.TravelPlanTestHelper; -import woowacourse.touroot.utils.DatabaseCleaner; - -import java.time.LocalDate; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("여행 계획 서비스") @Import(value = {TravelPlanService.class, TravelPlanTestHelper.class}) diff --git a/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java b/backend/src/test/java/kr/touroot/utils/DatabaseCleaner.java similarity index 98% rename from backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java rename to backend/src/test/java/kr/touroot/utils/DatabaseCleaner.java index ed2d2125..ada22125 100644 --- a/backend/src/test/java/woowacourse/touroot/utils/DatabaseCleaner.java +++ b/backend/src/test/java/kr/touroot/utils/DatabaseCleaner.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.utils; +package kr.touroot.utils; import jakarta.annotation.PostConstruct; import jakarta.persistence.Entity; diff --git a/backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java b/backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java deleted file mode 100644 index bc4f034b..00000000 --- a/backend/src/test/java/woowacourse/touroot/authentication/fixture/OauthUserInformationFixture.java +++ /dev/null @@ -1,12 +0,0 @@ -package woowacourse.touroot.authentication.fixture; - -import woowacourse.touroot.authentication.dto.response.OauthUserInformationResponse; -import woowacourse.touroot.authentication.dto.response.kakao.KakaoAccount; -import woowacourse.touroot.authentication.dto.response.kakao.KakaoProfile; - -public class OauthUserInformationFixture { - - public static final OauthUserInformationResponse USER_1_OAUTH_INFORMATION = new OauthUserInformationResponse( - 1L, new KakaoAccount(new KakaoProfile("리비", "img-url")) - ); -} From 56bf8cf8786881c99e023139889a2de3c89a7848 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:40:14 +0900 Subject: [PATCH 055/108] =?UTF-8?q?[Fix]=20-=20Authorization=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: JwtAuthFilter white list 로그인 method post로 변경 * fix: CORS 설정에 Authorization header 추가 * fix: JwtAuthFilter WhiteList에 cors preflight 허용 추가 * fix: LoginControllerTest disabled 처리 --- .../kr/touroot/global/auth/JwtAuthFilter.java | 11 +++++----- .../kr/touroot/global/config/WebConfig.java | 13 ++++++++++++ .../touroot/global/config/WebMvcConfig.java | 20 ------------------- .../controller/LoginControllerTest.java | 14 +++++++------ 4 files changed, 27 insertions(+), 31 deletions(-) delete mode 100644 backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index bc1d767b..f569e04c 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -1,12 +1,10 @@ -package woowacourse.touroot.global.auth; +package kr.touroot.global.auth; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.List; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; @@ -19,6 +17,9 @@ import org.springframework.web.filter.OncePerRequestFilter; import woowacourse.touroot.global.auth.dto.HttpRequestInfo; +import java.io.IOException; +import java.util.List; + @RequiredArgsConstructor @Slf4j @Component @@ -28,7 +29,6 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; private static final List WHITE_LIST = List.of( - new HttpRequestInfo(HttpMethod.GET, "/ping"), new HttpRequestInfo(HttpMethod.GET, "/h2-console/**"), new HttpRequestInfo(HttpMethod.POST, "/h2-console/**"), new HttpRequestInfo(HttpMethod.GET, "/favicon/**"), @@ -37,7 +37,8 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/**"), - new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**") + new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**"), + new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); @Override diff --git a/backend/src/main/java/kr/touroot/global/config/WebConfig.java b/backend/src/main/java/kr/touroot/global/config/WebConfig.java index bf0c2cb3..0ff87103 100644 --- a/backend/src/main/java/kr/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebConfig.java @@ -1,13 +1,26 @@ package kr.touroot.global.config; +import kr.touroot.global.auth.MemberAuthMethodArgumentResolver; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + +@RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { + private final MemberAuthMethodArgumentResolver memberAuthMethodArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(memberAuthMethodArgumentResolver); + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") diff --git a/backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java b/backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java deleted file mode 100644 index a29f0e30..00000000 --- a/backend/src/main/java/kr/touroot/global/config/WebMvcConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package kr.touroot.global.config; - -import java.util.List; -import kr.touroot.global.auth.MemberAuthMethodArgumentResolver; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@RequiredArgsConstructor -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - private final MemberAuthMethodArgumentResolver memberAuthMethodArgumentResolver; - - @Override - public void addArgumentResolvers(List resolvers) { - resolvers.add(memberAuthMethodArgumentResolver); - } -} diff --git a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java index f04d2e2e..9fef0233 100644 --- a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java @@ -1,14 +1,9 @@ package kr.touroot.authentication.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.fasterxml.jackson.databind.ObjectMapper; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.service.LoginService; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +13,13 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Disabled @WebMvcTest(LoginController.class) class LoginControllerTest { From 4d76184221e45f32181220eed662af8b91a7c39e Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:25:18 +0900 Subject: [PATCH 056/108] =?UTF-8?q?[Fix]=20-=20SSL=20key-store-password=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#163)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix; ssl key-store-password 변경 * fix: JwtAuthFilter 401 response에 cors 헤더 추가 (누락된 commit 복구) --- backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java | 1 + backend/src/main/resources/application.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index f569e04c..cf04468a 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -66,6 +66,7 @@ private void sendUnauthorizedResponse(HttpServletResponse response, String messa response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); + response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); response.getWriter() .write(objectMapper.writeValueAsString(errorResponse)); } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 55a7430c..a981bb99 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -49,7 +49,7 @@ security: server: ssl: key-store-type: PKCS12 - key-store-password: ENC(aIkhk+PERnL/OBpa2HtNyXAjOGbCZx9TJ+L5sekBxdI=) + key-store-password: ENC(faQYah2QoIaNVRZD9J6/junPRWkc5gaiAs+mEbxDk+I=) key-store: ENC(7VQCNdI7mXATwc4AiymZoyf3mz9SiskXpLnenpMSFBI=) spring: servlet: From 32635ad246e5527321943fab69a3db02881ca6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Wed, 31 Jul 2024 15:38:03 +0900 Subject: [PATCH 057/108] =?UTF-8?q?[Feature]=20-=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=85=EC=84=B8=20=EC=88=98=EC=A0=95=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20(#170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Login HTTP Method GET으로 변경 * style: 의미 없는 개행 제거 * fix: api 명세 수정 --- .../kr/touroot/authentication/controller/LoginController.java | 4 ++-- .../src/main/java/kr/touroot/global/auth/JwtAuthFilter.java | 2 +- backend/src/main/resources/application.yml | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java index a0a737c2..517b8ece 100644 --- a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java @@ -11,7 +11,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -36,7 +36,7 @@ public class LoginController { content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ) }) - @PostMapping("/oauth/kakao") + @GetMapping("/oauth/kakao") public ResponseEntity login(@RequestParam(name = "code") String authorizationCode) { return ResponseEntity.ok() .body(loginService.login(authorizationCode)); diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index cf04468a..296a25db 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -37,7 +37,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/**"), - new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**"), + new HttpRequestInfo(HttpMethod.GET, "/api/v1/login/**"), new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index a981bb99..f783b178 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -24,6 +24,9 @@ spring: config: activate: on-profile: local +oauth: + kakao: + redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao h2: console: enabled: true From 6540344d7a1fd5b79f58611ae83d9203c695cc3f Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:50:17 +0900 Subject: [PATCH 058/108] =?UTF-8?q?[Fix]=20-=20CORS=20config=EC=97=90?= =?UTF-8?q?=EC=84=9C=20allowedHeaders=20=EC=A0=9C=EA=B1=B0=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.gitignore | 1 + backend/src/main/java/kr/touroot/global/config/WebConfig.java | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 3409fd4e..187915be 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -3,3 +3,4 @@ build **/.DS_Store +/src/main/resources/keystore.p12 diff --git a/backend/src/main/java/kr/touroot/global/config/WebConfig.java b/backend/src/main/java/kr/touroot/global/config/WebConfig.java index 0ff87103..9c534466 100644 --- a/backend/src/main/java/kr/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebConfig.java @@ -3,7 +3,6 @@ import kr.touroot.global.auth.MemberAuthMethodArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -24,7 +23,6 @@ public void addArgumentResolvers(List resolvers) @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr") - .allowedHeaders(HttpHeaders.AUTHORIZATION); + .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr"); } } From e0dfd60dc68d361606bae3eeeedae09bfcbdbb38 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:35:57 +0900 Subject: [PATCH 059/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=AA=BD=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=A0=81=EC=9A=A9=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: package명 kr로 변경 * feat: TravelPlan에 Member 추가 * fix: application.yml dev profile 들여쓰기 오류 수정 * feat: 여행 계획 조회 시 인가 추가 * test: 여행 계획 작성자 인가 테스트 추가 * feat: TravelPlanController에 정상 요청 Swagger 응답 추가 * refactor: TravelPlanService가 검증에 대한 예외를 던지도록 수정 --- .../kr/touroot/global/auth/JwtAuthFilter.java | 3 +- .../MemberAuthMethodArgumentResolver.java | 2 +- .../global/auth/dto/HttpRequestInfo.java | 2 +- .../touroot/global/auth/dto/MemberAuth.java | 5 +- .../global/exception/ForbiddenException.java | 8 ++ .../exception/GlobalExceptionHandler.java | 10 +++ .../controller/TravelPlanController.java | 78 +++++++++++-------- .../touroot/travelplan/domain/TravelPlan.java | 31 +++++--- .../dto/request/TravelPlanCreateRequest.java | 6 +- .../travelplan/service/TravelPlanService.java | 39 ++++++++-- backend/src/main/resources/application.yml | 6 +- .../controller/TravelPlanControllerTest.java | 47 ++++++++--- .../travelplan/domain/TravelPlanTest.java | 33 ++++++-- .../helper/TravelPlanTestHelper.java | 13 ++-- .../service/TravelPlanServiceTest.java | 47 +++++++---- 15 files changed, 232 insertions(+), 98 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/global/exception/ForbiddenException.java diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index 296a25db..ce5e3361 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -15,7 +15,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import woowacourse.touroot.global.auth.dto.HttpRequestInfo; +import kr.touroot.global.auth.dto.HttpRequestInfo; import java.io.IOException; import java.util.List; @@ -36,7 +36,6 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/swagger-resources/**"), new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), - new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/login/**"), new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); diff --git a/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java index c0773b67..64dfa435 100644 --- a/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java +++ b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java @@ -7,7 +7,7 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import woowacourse.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.auth.dto.MemberAuth; @Component public class MemberAuthMethodArgumentResolver implements HandlerMethodArgumentResolver { diff --git a/backend/src/main/java/kr/touroot/global/auth/dto/HttpRequestInfo.java b/backend/src/main/java/kr/touroot/global/auth/dto/HttpRequestInfo.java index ec22bfb8..e9e91c23 100644 --- a/backend/src/main/java/kr/touroot/global/auth/dto/HttpRequestInfo.java +++ b/backend/src/main/java/kr/touroot/global/auth/dto/HttpRequestInfo.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.auth.dto; +package kr.touroot.global.auth.dto; import org.springframework.http.HttpMethod; diff --git a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java index 0bdc2d86..07ab4cea 100644 --- a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java +++ b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java @@ -1,4 +1,7 @@ -package woowacourse.touroot.global.auth.dto; +package kr.touroot.global.auth.dto; +import io.swagger.v3.oas.annotations.Hidden; + +@Hidden public record MemberAuth(Long memberId) { } diff --git a/backend/src/main/java/kr/touroot/global/exception/ForbiddenException.java b/backend/src/main/java/kr/touroot/global/exception/ForbiddenException.java new file mode 100644 index 00000000..2412a780 --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/exception/ForbiddenException.java @@ -0,0 +1,8 @@ +package kr.touroot.global.exception; + +public class ForbiddenException extends RuntimeException { + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index b4525cf1..b9a4b9f2 100644 --- a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -51,4 +52,13 @@ public ResponseEntity handleUploadExceedException(MaxUploadSi ExceptionResponse data = new ExceptionResponse("파일 업로드 용량을 초과하였습니다."); return ResponseEntity.badRequest().body(data); } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(ForbiddenException exception) { + log.warn("FORBIDDEN_EXCEPTION :: message = {}", exception.getMessage()); + + ExceptionResponse data = new ExceptionResponse(exception.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(data); + } } diff --git a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java index b73d7d89..e0d9dfeb 100644 --- a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java @@ -5,8 +5,10 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; @@ -14,12 +16,9 @@ import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; @Tag(name = "여행 계획") @RequiredArgsConstructor @@ -29,39 +28,56 @@ public class TravelPlanController { private final TravelPlanService travelPlanService; - @Operation( - summary = "여행 계획 생성", - responses = { - @ApiResponse( - responseCode = "400", - description = "Body에 유효하지 않은 값이 존재하거나 지난 날짜에 대한 계획을 생성할 때", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - } - ) + @Operation(summary = "여행 계획 생성") + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "여행 계획 생성이 정상적으로 성공했을 때" + ), + @ApiResponse( + responseCode = "400", + description = "Body에 유효하지 않은 값이 존재하거나 지난 날짜에 대한 계획을 생성할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 생성을 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) @PostMapping public ResponseEntity createTravelPlan( - @Valid @RequestBody TravelPlanCreateRequest request + @Valid @RequestBody TravelPlanCreateRequest request, + MemberAuth memberAuth ) { - TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request); - return ResponseEntity.ok(data); + TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request, memberAuth); + return ResponseEntity.created(URI.create("/api/v1/travel-plans/" + data.id())) + .body(data); } - @Operation( - summary = "여행 계획 상세 조회", - responses = { - @ApiResponse( - responseCode = "400", - description = "존재하지 않은 여행 계획을 조회할 때", - content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) - ) - } - ) + @Operation(summary = "여행 계획 상세 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "여행 계획 상세 조회가 정상적으로 성공했을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않은 여행 계획을 조회할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "작성자가 아닌 사용자가 조회를 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) @GetMapping("/{id}") public ResponseEntity readTravelPlan( - @Parameter(description = "여행 계획 id") @PathVariable Long id + @Parameter(description = "여행 계획 id") @PathVariable Long id, + MemberAuth memberAuth ) { - TravelPlanResponse data = travelPlanService.readTravelPlan(id); + TravelPlanResponse data = travelPlanService.readTravelPlan(id, memberAuth); return ResponseEntity.ok(data); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 40c5807d..7779c3a9 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -1,18 +1,15 @@ package kr.touroot.travelplan.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import java.time.LocalDate; +import jakarta.persistence.*; import kr.touroot.global.entity.BaseEntity; -import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDate; + @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -29,13 +26,23 @@ public class TravelPlan extends BaseEntity { @Column(nullable = false) private LocalDate startDate; + @JoinColumn(name = "author_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Member author; + public TravelPlan(String title, LocalDate startDate) { - this(null, title, startDate); + this(null, title, startDate, null); + } + + public TravelPlan(String title, LocalDate startDate, Member author) { + this(null, title, startDate, author); + } + + public boolean isValidStartDate() { + return startDate.isAfter(LocalDate.now()); } - public void validateStartDate() { - if (startDate.isBefore(LocalDate.now())) { - throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); - } + public boolean isAuthor(Member member) { + return member.equals(author); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java index b8a844a8..6e10304a 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java @@ -7,6 +7,8 @@ import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.util.List; + +import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; import lombok.Builder; @@ -25,7 +27,7 @@ public record TravelPlanCreateRequest( List days ) { - public TravelPlan toTravelPlan() { - return new TravelPlan(title, startDate); + public TravelPlan toTravelPlan(Member author) { + return new TravelPlan(title, startDate, author); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index ba057ce1..bc1b79eb 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -1,8 +1,10 @@ package kr.touroot.travelplan.service; -import java.util.Comparator; -import java.util.List; +import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; import kr.touroot.place.repository.PlaceRepository; import kr.touroot.travelplan.domain.TravelPlan; @@ -22,19 +24,24 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Comparator; +import java.util.List; + @RequiredArgsConstructor @Service public class TravelPlanService { + private final MemberRepository memberRepository; private final TravelPlanRepository travelPlanRepository; private final TravelPlanDayRepository travelPlanDayRepository; private final TravelPlanPlaceRepository travelPlanPlaceRepository; private final PlaceRepository placeRepository; @Transactional - public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request) { - TravelPlan travelPlan = request.toTravelPlan(); - travelPlan.validateStartDate(); + public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request, MemberAuth memberAuth) { + Member author = getMemberByMemberAuth(memberAuth); + TravelPlan travelPlan = request.toTravelPlan(author); + validStartDate(travelPlan); TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); createPlanDay(request.days(), savedTravelPlan); @@ -42,6 +49,17 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request return new TravelPlanCreateResponse(savedTravelPlan.getId()); } + private void validStartDate(TravelPlan travelPlan) { + if (!travelPlan.isValidStartDate()) { + throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); + } + } + + private Member getMemberByMemberAuth(MemberAuth memberAuth) { + return memberRepository.findById(memberAuth.memberId()) + .orElseThrow(() -> new BadRequestException("존재하지 않는 사용자입니다.")); + } + private void createPlanDay(List request, TravelPlan savedTravelPlan) { for (int order = 0; order < request.size(); order++) { PlanDayCreateRequest dayRequest = request.get(order); @@ -67,11 +85,20 @@ private Place getPlace(PlanPlaceCreateRequest planRequest) { } @Transactional(readOnly = true) - public TravelPlanResponse readTravelPlan(Long planId) { + public TravelPlanResponse readTravelPlan(Long planId, MemberAuth memberAuth) { TravelPlan travelPlan = getTravelPlanById(planId); + Member member = getMemberByMemberAuth(memberAuth); + validateAuthor(travelPlan, member); + return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } + private void validateAuthor(TravelPlan travelPlan, Member member) { + if (!travelPlan.isAuthor(member)) { + throw new ForbiddenException("여행 계획은 작성자만 조회할 수 있습니다."); + } + } + private TravelPlan getTravelPlanById(Long planId) { return travelPlanRepository.findById(planId) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index f783b178..57d3d3dc 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -20,13 +20,13 @@ security: token: secret-key: ENC(SNwFG2NQDZkmIK3nNoZFdwQ0ZxKuoe+qcw10ljdW941YEx/Qky9PEEl+wvAN9S1KR26D3a83SnU=) expire-length: 1800000 +oauth: + kakao: + redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao spring: config: activate: on-profile: local -oauth: - kakao: - redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao h2: console: enabled: true diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index e03c547f..561de6ef 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -1,11 +1,7 @@ package kr.touroot.travelplan.controller; -import static org.hamcrest.Matchers.is; - import io.restassured.RestAssured; import io.restassured.http.ContentType; -import java.time.LocalDate; -import java.util.List; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; @@ -22,6 +18,11 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; +import java.time.LocalDate; +import java.util.List; + +import static org.hamcrest.Matchers.is; + @DisplayName("여행 계획 컨트롤러") @AcceptanceTest class TravelPlanControllerTest { @@ -31,6 +32,8 @@ class TravelPlanControllerTest { private final DatabaseCleaner databaseCleaner; private final JwtTokenProvider jwtTokenProvider; private final TravelPlanTestHelper testHelper; + private String accessToken; + private Member member; @Autowired public TravelPlanControllerTest( @@ -47,6 +50,9 @@ public TravelPlanControllerTest( void setUp() { RestAssured.port = port; databaseCleaner.executeTruncate(); + + member = testHelper.initMemberTestData(); + accessToken = jwtTokenProvider.createToken(member.getId()); } @DisplayName("여행 계획 컨트롤러는 생성 요청이 들어올 때 200을 응답한다.") @@ -66,9 +72,6 @@ void createTravelPlan() { .days(List.of(planDayCreateRequest)) .build(); - Member member = testHelper.initMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); - // when & then RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -77,7 +80,7 @@ void createTravelPlan() { .when().log().all() .post("/api/v1/travel-plans") .then().log().all() - .statusCode(200) + .statusCode(201) .body("id", is(1)); } @@ -98,9 +101,6 @@ void createTravelPlanWithInvalidStartDate() { .days(List.of(planDayCreateRequest)) .build(); - Member member = testHelper.initMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); - // when & then RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -117,12 +117,13 @@ void createTravelPlanWithInvalidStartDate() { @Test void readTravelPlan() { // given - testHelper.initTravelPlanTestData(); + testHelper.initTravelPlanTestData(member); long id = 1L; // when & then RestAssured.given().log().all() .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .when().log().all() .get("/api/v1/travel-plans/" + id) .then().log().all() @@ -139,10 +140,32 @@ void readTravelPlanWithNonExist() { // when & then RestAssured.given().log().all() .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .when().log().all() .get("/api/v1/travel-plans/" + id) .then().log().all() .statusCode(400) .body("message", is("존재하지 않는 여행 계획입니다.")); } + + @DisplayName("여행 계획 컨트롤러는 작성자가 아닌 사용자가 조회 시 403을 응답한다.") + @Test + void readTravelPlanWithNotAuthor() { + // given + long id = testHelper.initTravelPlanTestData(member); + Member notAuthor = testHelper.initMemberTestData(); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + notAuthorAccessToken) + .when().log().all() + .get("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(403) + .body("message", is("여행 계획은 작성자만 조회할 수 있습니다.")); + + // then + } } diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index 5c86fb72..da4bb014 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -1,12 +1,13 @@ package kr.touroot.travelplan.domain; -import static org.assertj.core.api.Assertions.assertThatCode; - -import java.time.LocalDate; -import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + @DisplayName("여행 계획") class TravelPlanTest { @@ -16,9 +17,25 @@ void validateStartDate() { // given TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN); - // when & then - assertThatCode(travelPlan::validateStartDate) - .isInstanceOf(BadRequestException.class) - .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); + // when + boolean actual = travelPlan.isValidStartDate(); + + // then + assertThat(actual).isFalse(); + } + + @DisplayName("여행 계획은 작성자가 아닌 사용자가 검증을 시도하면 예외가 발생한다.") + @Test + void validateAuthor() { + // given + Member author = new Member(1L, 1L, "tester", "http://url.com"); + TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN, author); + Member notAuthor = new Member(2L, 2L, "tester2", "http://url.com"); + + // when + boolean actual = travelPlan.isAuthor(notAuthor); + + // then + assertThat(actual).isFalse(); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 47f1bd1e..8394b998 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -1,6 +1,5 @@ package kr.touroot.travelplan.helper; -import java.time.LocalDate; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; @@ -14,6 +13,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.time.LocalDate; + @Component public class TravelPlanTestHelper { @@ -46,8 +47,8 @@ public static Place getPlace(String name, String latitude, String longitude, Str return new Place(name, latitude, longitude, googlePlaceId); } - public static TravelPlan getTravelPlan(String title, LocalDate startDate) { - return new TravelPlan(title, startDate); + public static TravelPlan getTravelPlan(String title, LocalDate startDate, Member author) { + return new TravelPlan(title, startDate, author); } public static TravelPlanDay getTravelPlanDay(int order, TravelPlan travelPlan) { @@ -58,8 +59,8 @@ public static TravelPlanPlace getTravelPlanPlace(String description, int order, return new TravelPlanPlace(description, order, day, place); } - public void initTravelPlanTestData() { - TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX); + public long initTravelPlanTestData(Member author) { + TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); Place place = getPlace("장소", "37.5175896", "127.0867236", ""); TravelPlanPlace travelPlanPlace = getTravelPlanPlace("설명", 0, place, travelPlanDay); @@ -67,7 +68,7 @@ public void initTravelPlanTestData() { travelPlanRepository.save(travelPlan); travelPlanDayRepository.save(travelPlanDay); placeRepository.save(place); - travelPlanPlaceRepository.save(travelPlanPlace); + return travelPlanPlaceRepository.save(travelPlanPlace).getId(); } public Member initMemberTestData() { diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index 2b2e87e0..a9c8a1c1 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -1,12 +1,10 @@ package kr.touroot.travelplan.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.time.LocalDate; -import java.util.List; import kr.touroot.global.ServiceTest; +import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; +import kr.touroot.member.domain.Member; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; @@ -21,6 +19,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + @DisplayName("여행 계획 서비스") @Import(value = {TravelPlanService.class, TravelPlanTestHelper.class}) @ServiceTest @@ -30,6 +34,9 @@ class TravelPlanServiceTest { private final DatabaseCleaner databaseCleaner; private final TravelPlanTestHelper testHelper; + private MemberAuth memberAuth; + private Member author; + @Autowired public TravelPlanServiceTest( TravelPlanService travelPlanService, @@ -44,6 +51,9 @@ public TravelPlanServiceTest( @BeforeEach void setUp() { databaseCleaner.executeTruncate(); + + author = testHelper.initMemberTestData(); + memberAuth = new MemberAuth(author.getId()); } @DisplayName("여행 계획 서비스는 여행 계획 생성 시 생성된 id를 응답한다.") @@ -64,7 +74,7 @@ void createTravelPlan() { .build(); // when - TravelPlanCreateResponse actual = travelPlanService.createTravelPlan(request); + TravelPlanCreateResponse actual = travelPlanService.createTravelPlan(request, memberAuth); // then assertThat(actual.id()).isEqualTo(1L); @@ -88,7 +98,7 @@ void createTravelPlanWithInvalidStartDate() { .build(); // when & then= - assertThatThrownBy(() -> travelPlanService.createTravelPlan(request)) + assertThatThrownBy(() -> travelPlanService.createTravelPlan(request, memberAuth)) .isInstanceOf(BadRequestException.class) .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); } @@ -97,18 +107,16 @@ void createTravelPlanWithInvalidStartDate() { @Test void readTravelPlan() { // given - databaseCleaner.executeTruncate(); - testHelper.initTravelPlanTestData(); - Long id = 1L; + Long id = testHelper.initTravelPlanTestData(author); // when - TravelPlanResponse actual = travelPlanService.readTravelPlan(id); + TravelPlanResponse actual = travelPlanService.readTravelPlan(id, memberAuth); // then assertThat(actual.id()).isEqualTo(id); } - @DisplayName("여행 계획 서비스는 여행 계획 조회 시 상세 정보를 반환한다.") + @DisplayName("여행 계획 서비스는 존재하지 않는 여행 계획 조회 시 예외를 반환한다.") @Test void readTravelPlanWitNonExist() { // given @@ -116,8 +124,21 @@ void readTravelPlanWitNonExist() { Long id = 1L; // when & then - assertThatThrownBy(() -> travelPlanService.readTravelPlan(id)) + assertThatThrownBy(() -> travelPlanService.readTravelPlan(id, memberAuth)) .isInstanceOf(BadRequestException.class) .hasMessage("존재하지 않는 여행 계획입니다."); } + + @DisplayName("여행 계획 서비스는 작성자가 아닌 사용자가 조회 시 예외를 반환한다.") + @Test + void readTravelPlanWithNotAuthor() { + // given + Long id = testHelper.initTravelPlanTestData(author); + MemberAuth notAuthor = new MemberAuth(testHelper.initMemberTestData().getId()); + + // when & then + assertThatThrownBy(() -> travelPlanService.readTravelPlan(id, notAuthor)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("여행 계획은 작성자만 조회할 수 있습니다."); + } } From 8b4f93879c617feb7baf94b64ea9203cd44601a8 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:50:18 +0900 Subject: [PATCH 060/108] =?UTF-8?q?[Feature]=20-=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=9E=84=EC=9B=8C=ED=81=AC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: local/dev profile에서 show sql false로 변경 * feat: 401 exception logging 추가 및 attribute 이름 상수화 * feat: LoggingFilter 추가 * feat: loggback-spring.xml 작성 * fix: dev profile일 때 파일로 넣게 변경 * fix: logginFilter에서 userId memberId로 수정 * fix: HttpRequestInfo package 변경 * fix: HttpRequestInfo package 변경 * fix: .gitignore EOL 추가 * fix: logging 로직 dofilter 이후로 변경 --- backend/.gitignore | 1 + .../kr/touroot/global/auth/JwtAuthFilter.java | 6 +- .../MemberAuthMethodArgumentResolver.java | 2 +- .../exception/GlobalExceptionHandler.java | 6 +- .../touroot/global/logging/LoggingFilter.java | 61 +++++++++++++ backend/src/main/resources/application.yml | 4 +- backend/src/main/resources/logback-spring.xml | 86 +++++++++++++++++++ 7 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/global/logging/LoggingFilter.java create mode 100644 backend/src/main/resources/logback-spring.xml diff --git a/backend/.gitignore b/backend/.gitignore index 187915be..69da556b 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -3,4 +3,5 @@ build **/.DS_Store +/log /src/main/resources/keystore.p12 diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index ce5e3361..a76233b5 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -25,6 +25,8 @@ @Component public class JwtAuthFilter extends OncePerRequestFilter { + public static final String MEMBER_ID_ATTRIBUTE = "memberId"; + private final ObjectMapper objectMapper; private final JwtTokenProvider tokenProvider; @@ -52,7 +54,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse token = token.split("Bearer|bearer")[1]; try { String memberId = tokenProvider.decode(token); - request.setAttribute("memberId", memberId); + request.setAttribute(MEMBER_ID_ATTRIBUTE, memberId); filterChain.doFilter(request, response); } catch (Exception e) { sendUnauthorizedResponse(response, e.getMessage()); @@ -60,6 +62,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException { + log.warn("UNAUTHORIZED_EXCEPTION :: message = {}", message); + ExceptionResponse errorResponse = new ExceptionResponse(message); response.setStatus(HttpStatus.UNAUTHORIZED.value()); diff --git a/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java index 64dfa435..e43d947e 100644 --- a/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java +++ b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java @@ -25,7 +25,7 @@ public Object resolveArgument( WebDataBinderFactory binderFactory ) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String memberId = request.getAttribute("memberId").toString(); + String memberId = request.getAttribute(JwtAuthFilter.MEMBER_ID_ATTRIBUTE).toString(); return new MemberAuth(Long.valueOf(memberId)); } } diff --git a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index b9a4b9f2..33f52fc6 100644 --- a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -15,7 +15,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(BadRequestException exception) { - log.info("BAD_REQUEST_EXCEPTION :: message = {}", exception.getMessage()); + log.warn("BAD_REQUEST_EXCEPTION :: message = {}", exception.getMessage()); ExceptionResponse data = new ExceptionResponse(exception.getMessage()); return ResponseEntity.badRequest() @@ -26,7 +26,7 @@ public ResponseEntity handleBadRequestException(BadRequestExc public ResponseEntity handleMethodArgumentNotValidException( MethodArgumentNotValidException exception ) { - log.info("METHOD_ARGUMENT_NOT_VALID_EXCEPTION :: message = {}", exception.getMessage()); + log.warn("METHOD_ARGUMENT_NOT_VALID_EXCEPTION :: message = {}", exception.getMessage()); String message = exception.getBindingResult() .getAllErrors() @@ -47,7 +47,7 @@ public ResponseEntity handleClientException(ClientException e @ExceptionHandler(MaxUploadSizeExceededException.class) public ResponseEntity handleUploadExceedException(MaxUploadSizeExceededException exception) { - log.info("UPLOAD_SIZE_EXCEPTION :: message = {}", exception.getMessage()); + log.warn("UPLOAD_SIZE_EXCEPTION :: message = {}", exception.getMessage()); ExceptionResponse data = new ExceptionResponse("파일 업로드 용량을 초과하였습니다."); return ResponseEntity.badRequest().body(data); diff --git a/backend/src/main/java/kr/touroot/global/logging/LoggingFilter.java b/backend/src/main/java/kr/touroot/global/logging/LoggingFilter.java new file mode 100644 index 00000000..255efe8d --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/logging/LoggingFilter.java @@ -0,0 +1,61 @@ +package kr.touroot.global.logging; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import kr.touroot.global.auth.JwtAuthFilter; +import kr.touroot.global.auth.dto.HttpRequestInfo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Slf4j +@Component +public class LoggingFilter extends OncePerRequestFilter { + + private static final List WHITE_LIST = List.of( + new HttpRequestInfo(HttpMethod.GET, "/h2-console/**"), + new HttpRequestInfo(HttpMethod.POST, "/h2-console/**"), + new HttpRequestInfo(HttpMethod.GET, "/favicon/**"), + new HttpRequestInfo(HttpMethod.GET, "/swagger-ui/**"), + new HttpRequestInfo(HttpMethod.GET, "/swagger-resources/**"), + new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), + new HttpRequestInfo(HttpMethod.OPTIONS, "/**") + ); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + filterChain.doFilter(request, response); + + String method = request.getMethod(); + String url = request.getRequestURI(); + HttpStatus statusCode = HttpStatus.valueOf(response.getStatus()); + Object memberId = request.getAttribute(JwtAuthFilter.MEMBER_ID_ATTRIBUTE); + + if (memberId == null) { + log.info("{} {} ({})", method, url, statusCode); + return; + } + + log.info("{} {} ({}) :: userId = {}", method, url, statusCode, memberId); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + + String url = request.getRequestURI(); + String method = request.getMethod(); + + return WHITE_LIST.stream() + .anyMatch(white -> white.method().matches(method) && antPathMatcher.match(white.urlPattern(), url)); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 57d3d3dc..e1ba01d4 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -35,7 +35,7 @@ spring: url: jdbc:h2:mem:touroot username: sa jpa: - show-sql: true + show-sql: false properties: hibernate: format_sql: true @@ -71,7 +71,7 @@ spring: username: ENC(SJznQPqjlZuw3qf8kv9IJQ==) password: ENC(HsOo6wWp//egPPsSG6Wf40eF1Q2sVKfGuH4zGTL81Mw=) jpa: - show-sql: true + show-sql: false properties: hibernate: format_sql: true diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..1c1812ac --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,86 @@ + + + + + + + + ${LOG_PATTERN} + + + + + + INFO + ACCEPT + DENY + + + ./log/info/info-${NOW}.log + + + ${LOG_PATTERN} + + + ./log/backup/info/info-%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 3GB + + + + + + WARN + ACCEPT + DENY + + + ./log/warn/warn-${NOW}.log + + + ${LOG_PATTERN} + + + ./log/backup/warn/warn-%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 3GB + + + + + + ERROR + ACCEPT + DENY + + + ./log/error/error-${NOW}.log + + + ${LOG_PATTERN} + + + ./log/backup/error/error-%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 3GB + + + + + + + + + + + + + + + + + + From 2e5138314d51fc24565f01786dde8da3923fcbc1 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Thu, 1 Aug 2024 12:55:00 +0900 Subject: [PATCH 061/108] =?UTF-8?q?[Feature]=20-=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=A7=8C=20?= =?UTF-8?q?=EC=97=AC=ED=96=89=EA=B8=B0=EB=A5=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 패키지 구조 변경 * refactor: 인증된 사용자만 여행기를 작성할 수 있도록 리팩토링 * feat: 사용자 서비스 구현 * refactor: 여행기 관련 테스트에 사용자(작성자) 정보를 추가 * refactor: 로그인 되어 있지 않을 경우 발생하는 예외 타입 변경 * test: 로그인 되어 있지 않을 경우 발생하는 예외 테스트 작성 * chore: 불필요한 어노테이션 제거 * chore: MemberAuth validation 추가 * fix: 변경된 패키지에 맞게 import 구문 수정 * refactor: Project group 수정 --- backend/build.gradle | 2 +- .../infrastructure/JwtTokenProvider.java | 7 ++-- .../kr/touroot/global/auth/JwtAuthFilter.java | 2 +- .../MemberAuthMethodArgumentResolver.java | 11 +++++-- .../touroot/global/auth/dto/MemberAuth.java | 5 ++- .../exception/UnauthorizedException.java | 2 +- .../java/kr/touroot/member/domain/Member.java | 5 +-- .../touroot/member/service/MemberService.java | 19 +++++++++++ .../controller/TravelogueController.java | 8 +++-- .../touroot/travelogue/domain/Travelogue.java | 12 +++++-- .../dto/request/TravelogueRequest.java | 5 +-- .../service/TravelogueFacadeService.java | 9 ++++-- .../travelogue/service/TravelogueService.java | 5 +-- .../controller/TravelogueControllerTest.java | 13 ++++++++ .../travelogue/fixture/TravelogueFixture.java | 14 ++++++-- .../helper/TravelogueTestHelper.java | 14 ++++++-- .../service/TravelogueDayServiceTest.java | 7 ++-- .../service/TravelogueFacadeServiceTest.java | 8 ++++- .../service/TraveloguePhotoServiceTest.java | 7 ++-- .../service/TraveloguePlaceServiceTest.java | 32 ++++++++++--------- .../service/TravelogueServiceTest.java | 5 ++- 21 files changed, 141 insertions(+), 51 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/member/service/MemberService.java diff --git a/backend/build.gradle b/backend/build.gradle index 3ae119d3..3aa25d0a 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -4,7 +4,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.5' } -group = 'woowacourse' +group = 'kr.touroot' version = '0.0.1-SNAPSHOT' java { diff --git a/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java index 4cd7ec76..56521101 100644 --- a/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java @@ -3,12 +3,11 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import woowacourse.touroot.global.exception.UnauthorizedException; - import java.nio.charset.StandardCharsets; import java.util.Date; +import kr.touroot.global.exception.UnauthorizedException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; @Component public class JwtTokenProvider { diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index a76233b5..e3ff3c14 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.global.auth.dto.HttpRequestInfo; import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,7 +16,6 @@ import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import kr.touroot.global.auth.dto.HttpRequestInfo; import java.io.IOException; import java.util.List; diff --git a/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java index e43d947e..4242f56d 100644 --- a/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java +++ b/backend/src/main/java/kr/touroot/global/auth/MemberAuthMethodArgumentResolver.java @@ -1,13 +1,15 @@ package kr.touroot.global.auth; import jakarta.servlet.http.HttpServletRequest; +import java.util.Objects; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.exception.UnauthorizedException; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import kr.touroot.global.auth.dto.MemberAuth; @Component public class MemberAuthMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -25,7 +27,12 @@ public Object resolveArgument( WebDataBinderFactory binderFactory ) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - String memberId = request.getAttribute(JwtAuthFilter.MEMBER_ID_ATTRIBUTE).toString(); + String memberId = Objects.requireNonNull(request).getAttribute("memberId").toString(); + + if (memberId == null) { + throw new UnauthorizedException("로그인이 필요합니다."); + } + return new MemberAuth(Long.valueOf(memberId)); } } diff --git a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java index 07ab4cea..d2dce76c 100644 --- a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java +++ b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java @@ -1,7 +1,6 @@ package kr.touroot.global.auth.dto; -import io.swagger.v3.oas.annotations.Hidden; +import jakarta.validation.constraints.NotNull; -@Hidden -public record MemberAuth(Long memberId) { +public record MemberAuth(@NotNull Long memberId) { } diff --git a/backend/src/main/java/kr/touroot/global/exception/UnauthorizedException.java b/backend/src/main/java/kr/touroot/global/exception/UnauthorizedException.java index 5b7fed83..b5c35538 100644 --- a/backend/src/main/java/kr/touroot/global/exception/UnauthorizedException.java +++ b/backend/src/main/java/kr/touroot/global/exception/UnauthorizedException.java @@ -1,4 +1,4 @@ -package woowacourse.touroot.global.exception; +package kr.touroot.global.exception; public class UnauthorizedException extends RuntimeException { diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index 8f9384f0..4f7aa092 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -5,11 +5,11 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import kr.touroot.global.entity.BaseEntity; -import kr.touroot.global.exception.BadRequestException; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -18,6 +18,7 @@ public class Member extends BaseEntity { public static final int NICKNAME_MIN_LENGTH = 1; public static final int NICKNAME_MAX_LENGTH = 20; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/backend/src/main/java/kr/touroot/member/service/MemberService.java b/backend/src/main/java/kr/touroot/member/service/MemberService.java new file mode 100644 index 00000000..5d05fe40 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/service/MemberService.java @@ -0,0 +1,19 @@ +package kr.touroot.member.service; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class MemberService { + + private final MemberRepository memberRepository; + + public Member getById(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new BadRequestException("존재하지 않는 사용자입니다.")); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index 91c7dc70..ec3a22b1 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.net.URI; +import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; @@ -48,8 +49,11 @@ public class TravelogueController { ), }) @PostMapping - public ResponseEntity createTravelogue(@Valid @RequestBody TravelogueRequest request) { - TravelogueResponse response = travelogueFacadeService.createTravelogue(request); + public ResponseEntity createTravelogue( + @Valid MemberAuth member, + @Valid @RequestBody TravelogueRequest request + ) { + TravelogueResponse response = travelogueFacadeService.createTravelogue(member, request); return ResponseEntity.created(URI.create("/api/v1/travelogues/" + response.id())) .body(response); diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index 0743e7c4..6d54392f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -2,10 +2,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.member.domain.Member; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -23,13 +27,17 @@ public class Travelogue extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Member author; + @Column(nullable = false, length = 20) private String title; @Column(nullable = false) private String thumbnail; - public Travelogue(String title, String thumbnail) { - this(null, title, thumbnail); + public Travelogue(Member author, String title, String thumbnail) { + this(null, author, title, thumbnail); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index fb59d22a..623bac1e 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.List; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; public record TravelogueRequest( @@ -19,7 +20,7 @@ public record TravelogueRequest( List days ) { - public Travelogue toTravelogue() { - return new Travelogue(title, thumbnail); + public Travelogue toTravelogueOf(Member author) { + return new Travelogue(author, title, thumbnail); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index ab3809a6..9dd77878 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -3,6 +3,9 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.member.domain.Member; +import kr.touroot.member.service.MemberService; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePhoto; @@ -29,10 +32,12 @@ public class TravelogueFacadeService { private final TravelogueDayService travelogueDayService; private final TraveloguePlaceService traveloguePlaceService; private final TraveloguePhotoService traveloguePhotoService; + private final MemberService memberService; @Transactional - public TravelogueResponse createTravelogue(TravelogueRequest request) { - Travelogue travelogue = travelogueService.createTravelogue(request); + public TravelogueResponse createTravelogue(MemberAuth member, TravelogueRequest request) { + Member author = memberService.getById(member.memberId()); + Travelogue travelogue = travelogueService.createTravelogue(author, request); return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue)); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index c519a808..4b97d4f2 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -1,6 +1,7 @@ package kr.touroot.travelogue.service; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.repository.TravelogueRepository; @@ -15,8 +16,8 @@ public class TravelogueService { private final TravelogueRepository travelogueRepository; - public Travelogue createTravelogue(TravelogueRequest request) { - Travelogue travelogue = request.toTravelogue(); + public Travelogue createTravelogue(Member author, TravelogueRequest request) { + Travelogue travelogue = request.toTravelogueOf(author); return travelogueRepository.save(travelogue); } diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 610e3a50..7f1f7937 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -71,6 +71,19 @@ void createTravelogue() { .header("Location", "/api/v1/travelogues/1"); } + @DisplayName("여행기를 작성할 때 로그인 되어 있지 않으면 예외가 발생한다.") + @Test + void createTravelogueWithNotLoginThrowException() { + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/travelogues") + .then().log().all() + .statusCode(401); + } + @DisplayName("여행기를 상세 조회한다.") @Test void findTravelogue() throws JsonProcessingException { diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java index 5de3f0b6..84b5de8a 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java @@ -1,20 +1,28 @@ package kr.touroot.travelogue.fixture; +import kr.touroot.authentication.fixture.MemberFixture; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; public enum TravelogueFixture { - TRAVELOGUE("제주에 하영 옵서", "https://url.com/jeju_thumbnail.png"); + TRAVELOGUE(MemberFixture.MEMBER_1, "제주에 하영 옵서", "https://url.com/jeju_thumbnail.png"); + private final Member author; private final String title; private final String thumbnail; - TravelogueFixture(String title, String thumbnail) { + TravelogueFixture(Member author, String title, String thumbnail) { + this.author = author; this.title = title; this.thumbnail = thumbnail; } public Travelogue get() { - return new Travelogue(title, thumbnail); + return new Travelogue(author, title, thumbnail); + } + + public Travelogue create(Member author) { + return new Travelogue(author, title, thumbnail); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 06d979a5..00519acd 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -6,6 +6,7 @@ import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; +import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; @@ -49,15 +50,22 @@ public TravelogueTestHelper( } public void initTravelogueTestData() { - Travelogue travelogue = persistTravelogue(); + Member author = persistMember(); + Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); Place position = persistPlace(); TraveloguePlace place = persistTraveloguePlace(position, day); persistTraveloguePhoto(place); } - public Travelogue persistTravelogue() { - Travelogue travelogue = TRAVELOGUE.get(); + public Member persistMember() { + Member author = MemberFixture.MEMBER_1; + + return memberRepository.save(author); + } + + public Travelogue persistTravelogue(Member author) { + Travelogue travelogue = TRAVELOGUE.create(author); return travelogueRepository.save(travelogue); } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java index fae4b714..f3b4e4b0 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -9,6 +9,7 @@ import java.util.Map; import kr.touroot.global.ServiceTest; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; @@ -51,7 +52,8 @@ void setUp() { @Test void createDays() { List requests = TravelogueRequestFixture.getTravelogueDayRequests(); - Travelogue travelogue = testHelper.persistTravelogue(); + Member author = testHelper.persistMember(); + Travelogue travelogue = testHelper.persistTravelogue(author); Map> daysMap = dayService.createDays(requests, travelogue); List days = daysMap.keySet().stream().toList(); @@ -65,7 +67,8 @@ void createDays() { @DisplayName("여행기를 기준으로 여행 일자들을 조회한다.") @Test void findDaysByTravelogue() { - Travelogue travelogue = testHelper.persistTravelogue(); + Member author = testHelper.persistMember(); + Travelogue travelogue = testHelper.persistTravelogue(author); TravelogueDay travelogueDay = testHelper.persistTravelogueDay(travelogue); List days = dayService.findDaysByTravelogue(travelogue); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 5f63ebe7..d66ed830 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import kr.touroot.global.ServiceTest; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.member.service.MemberService; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; @@ -24,6 +26,7 @@ TraveloguePhotoService.class, TravelogueDayService.class, TraveloguePlaceService.class, + MemberService.class, TravelogueTestHelper.class, }) @ServiceTest @@ -52,9 +55,12 @@ public TravelogueFacadeServiceTest( @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { + testHelper.initMemberTestData(); + + MemberAuth memberAuth = new MemberAuth(1L); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); - assertThat(service.createTravelogue(request)) + assertThat(service.createTravelogue(memberAuth, request)) .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java index 6c91f567..f5eb8c4d 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -4,6 +4,7 @@ import java.util.List; import kr.touroot.global.ServiceTest; +import kr.touroot.member.domain.Member; import kr.touroot.place.domain.Place; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; @@ -35,7 +36,8 @@ public TraveloguePhotoServiceTest(TraveloguePhotoService photoService, Travelogu @Test void createPhotos() { List requests = TravelogueRequestFixture.getTraveloguePhotoRequests(); - Travelogue travelogue = testHelper.persistTravelogue(); + Member author = testHelper.persistMember(); + Travelogue travelogue = testHelper.persistTravelogue(author); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); Place position = testHelper.persistPlace(); TraveloguePlace place = testHelper.persistTraveloguePlace(position, day); @@ -48,7 +50,8 @@ void createPhotos() { @DisplayName("여행기 사진 URL을 여행기 장소를 기준으로 조회한다.") @Test void findPhotoUrlsByPlace() { - Travelogue travelogue = testHelper.persistTravelogue(); + Member author = testHelper.persistMember(); + Travelogue travelogue = testHelper.persistTravelogue(author); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); Place position = testHelper.persistPlace(); TraveloguePlace place = testHelper.persistTraveloguePlace(position, day); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 833add72..f70525ce 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -1,12 +1,15 @@ package kr.touroot.travelogue.service; -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.context.annotation.Import; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.util.List; +import java.util.Map; import kr.touroot.global.ServiceTest; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; import kr.touroot.place.domain.Place; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; @@ -16,14 +19,11 @@ import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; import kr.touroot.utils.DatabaseCleaner; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.context.annotation.Import; @DisplayName("여행기 장소 서비스") @Import(value = {TraveloguePlaceService.class, TravelogueTestHelper.class}) @@ -54,7 +54,8 @@ void setUp() { @Test void createPlaces() { List requests = TravelogueRequestFixture.getTraveloguePlaceRequests(); - Travelogue travelogue = testHelper.persistTravelogue(); + Member author = testHelper.persistMember(); + Travelogue travelogue = testHelper.persistTravelogue(author); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); Map> placesMap = placeService.createPlaces(requests, day); @@ -69,7 +70,8 @@ void createPlaces() { @DisplayName("여행기 장소를 여행기 일자를 기준으로 조회한다.") @Test void findTraveloguePlacesByDay() { - Travelogue travelogue = testHelper.persistTravelogue(); + Member author = testHelper.persistMember(); + Travelogue travelogue = testHelper.persistTravelogue(author); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); Place position = testHelper.persistPlace(); TraveloguePlace place = testHelper.persistTraveloguePlace(position, day); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 6c47150a..1c573a34 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -7,6 +7,7 @@ import kr.touroot.global.ServiceTest; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; @@ -49,8 +50,10 @@ public TravelogueServiceTest( @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { + Member author = testHelper.persistMember(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); - Travelogue createdTravelogue = travelogueService.createTravelogue(request); + + Travelogue createdTravelogue = travelogueService.createTravelogue(author, request); assertAll( () -> assertThat(createdTravelogue.getId()).isEqualTo(1L), From 7e5c5359e4a0248dabd5889131b0d72b1b6f441b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 1 Aug 2024 13:56:59 +0900 Subject: [PATCH 062/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0,=20=EC=97=AC=ED=96=89=EA=B3=84=ED=9A=8D=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B2=80=EC=A6=9D=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 멤버 프로필 이미지 경로 검증 추가 * fix: 멤버 도메인 검증 규칙 추가에 의한 fixture 수정 * feat: 여행기 DTO 검증 조건 추가 * feat: 여행기 도메인 검증 추가 * feat: 장소 도메인 검증 구현 * feat: 여행기 날짜 검증 로직 구현 * feat: 여행 장소 사진 도메인 검증 구현 * feat: 여행 장소 도메인 검증 구현 * feat: 여행 계획 날짜 검증 구현 * feat: 여행 계획 검증 작성 * feat: 여행 계획 장소 검증 구현 * fix: 로그인 컨트롤러 테스트 수정 * fix: 여행기 제목 검증과 여행 계획 제목 검증 통일 --- .../dto/response/LoginResponse.java | 2 +- .../kr/touroot/global/config/WebConfig.java | 3 +- .../java/kr/touroot/member/domain/Member.java | 36 +++--- .../java/kr/touroot/place/domain/Place.java | 52 ++++++++- .../touroot/travelogue/domain/Travelogue.java | 48 +++++++- .../travelogue/domain/TravelogueDay.java | 27 ++++- .../travelogue/domain/TraveloguePhoto.java | 28 ++++- .../travelogue/domain/TraveloguePlace.java | 42 ++++++- .../dto/request/TraveloguePhotoRequest.java | 4 +- .../dto/request/TraveloguePlaceRequest.java | 6 +- .../request/TraveloguePositionRequest.java | 6 +- .../dto/request/TravelogueRequest.java | 7 +- .../touroot/travelplan/domain/TravelPlan.java | 56 ++++++++-- .../travelplan/domain/TravelPlanDay.java | 33 +++++- .../travelplan/domain/TravelPlanPlace.java | 50 +++++++-- .../controller/LoginControllerTest.java | 24 ++-- .../authentication/fixture/MemberFixture.java | 2 +- .../fixture/OauthUserInformationFixture.java | 2 +- .../kr/touroot/member/domain/MemberTest.java | 26 +++-- .../kr/touroot/place/domain/PlaceTest.java | 104 ++++++++++++++++++ .../travelogue/domain/TravelogueDayTest.java | 50 +++++++++ .../domain/TraveloguePhotoTest.java | 59 ++++++++++ .../domain/TraveloguePlaceTest.java | 80 ++++++++++++++ .../travelogue/domain/TravelogueTest.java | 87 +++++++++++++++ .../helper/TravelogueTestHelper.java | 2 +- .../travelplan/domain/TravelPlanDayTest.java | 50 +++++++++ .../domain/TravelPlanPlaceTest.java | 79 +++++++++++++ .../travelplan/domain/TravelPlanTest.java | 65 ++++++++++- .../fixture/TravelPlanDayFixture.java | 23 ++++ .../travelplan/fixture/TravelPlanFixture.java | 25 +++++ .../helper/TravelPlanTestHelper.java | 2 +- 31 files changed, 995 insertions(+), 85 deletions(-) create mode 100644 backend/src/test/java/kr/touroot/place/domain/PlaceTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePhotoTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanDayTest.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java diff --git a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java index 6bda6ff1..3f88cc6d 100644 --- a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java @@ -9,6 +9,6 @@ public record LoginResponse( @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") String accessToken) { public static LoginResponse of(Member member, String accessToken) { - return new LoginResponse(member.getNickname(), member.getProfileImageUri(), accessToken); + return new LoginResponse(member.getNickname(), member.getProfileImageUrl(), accessToken); } } diff --git a/backend/src/main/java/kr/touroot/global/config/WebConfig.java b/backend/src/main/java/kr/touroot/global/config/WebConfig.java index 9c534466..25fe59d5 100644 --- a/backend/src/main/java/kr/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebConfig.java @@ -1,5 +1,6 @@ package kr.touroot.global.config; +import java.util.List; import kr.touroot.global.auth.MemberAuthMethodArgumentResolver; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -7,8 +8,6 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.util.List; - @RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index 4f7aa092..0908d8e1 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -5,6 +5,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.net.URL; import kr.touroot.global.entity.BaseEntity; import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; @@ -30,34 +31,35 @@ public class Member extends BaseEntity { private String nickname; @Column(nullable = false) - private String profileImageUri; + private String profileImageUrl; - public Member(Long id, Long kakaoId, String nickname, String profileImageUri) { - validate(kakaoId, nickname, profileImageUri); + public Member(Long id, Long kakaoId, String nickname, String profileImageUrl) { + validate(kakaoId, nickname, profileImageUrl); this.id = id; this.kakaoId = kakaoId; this.nickname = nickname; - this.profileImageUri = profileImageUri; + this.profileImageUrl = profileImageUrl; } - public Member(Long kakaoId, String nickname, String profileImageUri) { - this(null, kakaoId, nickname, profileImageUri); + public Member(Long kakaoId, String nickname, String profileImageUrl) { + this(null, kakaoId, nickname, profileImageUrl); } - private void validate(Long kakaoId, String nickname, String profileImageUri) { - validateNotNull(kakaoId, nickname, profileImageUri); - validateNotBlank(nickname, profileImageUri); + private void validate(Long kakaoId, String nickname, String profileImageUrl) { + validateNotNull(kakaoId, nickname, profileImageUrl); + validateNotBlank(nickname, profileImageUrl); validateNicknameLength(nickname); + validateProfileImageUrl(profileImageUrl); } - private void validateNotNull(Long kakaoId, String nickname, String profileImageUri) { - if (kakaoId == null || nickname == null || profileImageUri == null) { + private void validateNotNull(Long kakaoId, String nickname, String profileImageUrl) { + if (kakaoId == null || nickname == null || profileImageUrl == null) { throw new BadRequestException("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } } - private void validateNotBlank(String nickname, String profileImageUri) { - if (nickname.isBlank() || profileImageUri.isBlank()) { + private void validateNotBlank(String nickname, String profileImageUrl) { + if (nickname.isBlank() || profileImageUrl.isBlank()) { throw new BadRequestException("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } } @@ -69,4 +71,12 @@ private void validateNicknameLength(String nickname) { ); } } + + private void validateProfileImageUrl(String profileImageUrl) { + try { + new URL(profileImageUrl).toURI(); + } catch (Exception e) { + throw new BadRequestException("이미지 url 형식이 잘못되었습니다"); + } + } } diff --git a/backend/src/main/java/kr/touroot/place/domain/Place.java b/backend/src/main/java/kr/touroot/place/domain/Place.java index c0053214..cf8dac71 100644 --- a/backend/src/main/java/kr/touroot/place/domain/Place.java +++ b/backend/src/main/java/kr/touroot/place/domain/Place.java @@ -5,18 +5,24 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class Place extends BaseEntity { + private static final Pattern LATITUDE_PATTERN = Pattern.compile("^([-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?))$"); + private static final Pattern LONGITUDE_PATTERN = Pattern.compile( + "^([-+]?((1[0-7]\\d(\\.\\d+)?|180(\\.0+)?)|([1-9]?\\d(\\.\\d+)?)))$"); + private static final int PLACE_NAME_MAX_LENGTH = 60; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -32,6 +38,15 @@ public class Place extends BaseEntity { private String googlePlaceId; + public Place(Long id, String name, String latitude, String longitude, String googlePlaceId) { + validate(name, latitude, longitude, googlePlaceId); + this.id = id; + this.name = name; + this.latitude = latitude; + this.longitude = longitude; + this.googlePlaceId = googlePlaceId; + } + public Place(String name, String latitude, String longitude, String googlePlaceId) { this(null, name, latitude, longitude, googlePlaceId); } @@ -39,4 +54,37 @@ public Place(String name, String latitude, String longitude, String googlePlaceI public Place(String name, String latitude, String longitude) { this(null, name, latitude, longitude, null); } + + private void validate(String name, String latitude, String longitude, String googlePlaceId) { + validateNotNull(name, latitude, longitude); + validateBlank(name, latitude, longitude); + validateLatitudeLongitudeFormat(latitude, longitude); + validatePlaceNameLength(name); + } + + private void validateNotNull(String name, String latitude, String longitude) { + if (name == null || latitude == null || longitude == null) { + throw new BadRequestException("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + } + + private void validateBlank(String name, String latitude, String longitude) { + if (name.isBlank() || latitude.isBlank() || longitude.isBlank()) { + throw new BadRequestException("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + } + + private void validateLatitudeLongitudeFormat(String latitude, String longitude) { + Matcher latitudeMatcher = LATITUDE_PATTERN.matcher(latitude); + Matcher longitudeMatcher = LONGITUDE_PATTERN.matcher(longitude); + if (!latitudeMatcher.find() || !longitudeMatcher.find()) { + throw new BadRequestException("위,경도의 형식이 올바르지 않습니다"); + } + } + + private void validatePlaceNameLength(String placeName) { + if (placeName.length() > PLACE_NAME_MAX_LENGTH) { + throw new BadRequestException("장소 이름은 " + PLACE_NAME_MAX_LENGTH + "자 이하여야 합니다"); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index 6d54392f..66028950 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -8,10 +8,11 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.net.URL; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -19,10 +20,12 @@ @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class Travelogue extends BaseEntity { + private static final int MIN_TITLE_LENGTH = 1; + private static final int MAX_TITLE_LENGTH = 20; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -37,7 +40,48 @@ public class Travelogue extends BaseEntity { @Column(nullable = false) private String thumbnail; + public Travelogue(Long id, Member author, String title, String thumbnail) { + validate(author, title, thumbnail); + this.id = id; + this.author = author; + this.title = title; + this.thumbnail = thumbnail; + } + public Travelogue(Member author, String title, String thumbnail) { this(null, author, title, thumbnail); } + + private void validate(Member author, String title, String thumbnail) { + validateNotNull(author, title, thumbnail); + validateNotBlank(title, thumbnail); + validateTitleLength(title); + validateThumbnailFormat(thumbnail); + } + + private void validateNotNull(Member author, String title, String thumbnail) { + if (title == null || thumbnail == null || author == null) { + throw new BadRequestException("작성자, 여행기 제목, 그리고 여행기 썸네일은 비어있을 수 없습니다"); + } + } + + private void validateNotBlank(String title, String thumbnail) { + if (title.isBlank() || thumbnail.isBlank()) { + throw new BadRequestException("여행기 제목, 여행기 썸네일은 비어있을 수 없습니다"); + } + } + + private void validateTitleLength(String title) { + if (MIN_TITLE_LENGTH > title.length() || title.length() > MAX_TITLE_LENGTH) { + throw new BadRequestException("여행기 제목은 " + MIN_TITLE_LENGTH + "자 이상, " + MAX_TITLE_LENGTH + "자 이하여야 합니다"); + } + } + + private void validateThumbnailFormat(String thumbnailUrl) { + try { + new URL(thumbnailUrl).toURI(); + } catch (Exception e) { + throw new BadRequestException("이미지 url 형식이 잘못되었습니다"); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java index e8d72aea..fe3c15df 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java @@ -9,8 +9,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,7 +18,6 @@ @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TravelogueDay extends BaseEntity { @@ -33,7 +32,31 @@ public class TravelogueDay extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private Travelogue travelogue; + private TravelogueDay(Long id, Integer order, Travelogue travelogue) { + validate(order, travelogue); + this.id = id; + this.order = order; + this.travelogue = travelogue; + } + public TravelogueDay(Integer order, Travelogue travelogue) { this(null, order, travelogue); } + + private void validate(Integer order, Travelogue travelogue) { + validateNotNull(order, travelogue); + validateOrderRange(order); + } + + private void validateNotNull(Integer order, Travelogue travelogue) { + if (order == null || travelogue == null) { + throw new BadRequestException("여행 날짜가 속한 여행기와 여행 날짜의 순서는 비어 있을 수 없습니다"); + } + } + + private void validateOrderRange(Integer order) { + if (order < 0) { + throw new BadRequestException("여행 날짜의 순서는 음수 일 수 없습니다"); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java index 0851dd4e..39aaf6e1 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java @@ -9,8 +9,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,7 +18,6 @@ @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TraveloguePhoto extends BaseEntity { @@ -36,7 +35,32 @@ public class TraveloguePhoto extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private TraveloguePlace traveloguePlace; + private TraveloguePhoto(Long id, String key, Integer order, TraveloguePlace traveloguePlace) { + validate(order, key, traveloguePlace); + this.id = id; + this.key = key; + this.order = order; + this.traveloguePlace = traveloguePlace; + } + public TraveloguePhoto(Integer order, String key, TraveloguePlace traveloguePlace) { this(null, key, order, traveloguePlace); } + + private void validate(Integer order, String key, TraveloguePlace traveloguePlace) { + validateNotNull(order, key, traveloguePlace); + validateOrderRange(order); + } + + private void validateNotNull(Integer order, String key, TraveloguePlace traveloguePlace) { + if (order == null || key == null || traveloguePlace == null) { + throw new BadRequestException("여행 장소의 사진에서 순서와 키, 그리고 사진이 속한 여행 장소는 비어 있을 수 없습니다"); + } + } + + private void validateOrderRange(Integer order) { + if (order < 0) { + throw new BadRequestException("여행 장소에 속하는 사진의 순서는 음수가 될 수 없습니다"); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index 3c6635e4..398caae0 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -8,21 +8,22 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; -import kr.touroot.global.entity.BaseEntity; -import kr.touroot.place.domain.Place; @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TraveloguePlace extends BaseEntity { + private static final int MAX_DESCRIPTION_LENGTH = 300; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -40,10 +41,43 @@ public class TraveloguePlace extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private TravelogueDay travelogueDay; + private TraveloguePlace(Long id, Integer order, String description, Place place, TravelogueDay travelogueDay) { + validate(order, description, place, travelogueDay); + this.id = id; + this.order = order; + this.description = description; + this.place = place; + this.travelogueDay = travelogueDay; + } + public TraveloguePlace(Integer order, String description, Place place, TravelogueDay travelogueDay) { this(null, order, description, place, travelogueDay); } + private void validate(Integer order, String description, Place place, TravelogueDay travelogueDay) { + validateNotNull(order, place, travelogueDay); + validateOrderRange(order); + validateDescriptionLength(description); + } + + private void validateNotNull(Integer order, Place place, TravelogueDay travelogueDay) { + if (order == null || place == null || travelogueDay == null) { + throw new BadRequestException("여행기 장소에서 순서와 장소 상세 정보, 그리고 방문 날짜는 비어 있을 수 없습니다"); + } + } + + private void validateOrderRange(Integer order) { + if (order < 0) { + throw new BadRequestException("여행 장소의 순서는 음수일 수 없습니다"); + } + } + + private void validateDescriptionLength(String description) { + if (description != null && description.length() > MAX_DESCRIPTION_LENGTH) { + throw new BadRequestException("여행 장소에 대한 설명은 " + MAX_DESCRIPTION_LENGTH + "자를 넘길 수 없습니다"); + } + } + public String getName() { return place.getName(); } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java index 68f1fc26..b1e82852 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java @@ -1,13 +1,13 @@ package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; public record TraveloguePhotoRequest( @Schema(description = "여행기 장소 사진 URL", example = "photo.png") - @NotNull(message = "여행기 장소 사진 URL 값은 비어있을 수 없습니다.") + @NotBlank(message = "여행기 장소 사진 URL 값은 비어있을 수 없습니다.") String url ) { diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index 88997afa..f7d8ec63 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -2,7 +2,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.util.List; import kr.touroot.place.domain.Place; import kr.touroot.travelogue.domain.TravelogueDay; @@ -10,13 +12,15 @@ public record TraveloguePlaceRequest( @Schema(description = "여행기 장소 이름", example = "선릉 캠퍼스") - @NotNull(message = "여행기 장소 이름은 비어있을 수 없습니다.") + @NotBlank(message = "여행기 장소 이름은 비어있을 수 없습니다.") + @Size(message = "장소 이름은 60자를 초과할 수 없습니다", max = 60) String placeName, @Schema(description = "여행기 장소 위치 정보") @NotNull(message = "여행기 장소 위치 정보는 비어있을 수 없습니다.") @Valid TraveloguePositionRequest position, @Schema(description = "여행기 장소 설명", example = "성담 빌딩에 위치한 선릉 캠퍼스입니다.") + @Size(message = "장소 설명은 300글자 이하입니다.", max = 300) String description, @Schema(description = "여행기 장소 사진") @NotNull(message = "여행기 장소 사진은 비어있을 수 없습니다.") diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java index aa7ee56e..ab42446c 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePositionRequest.java @@ -1,14 +1,14 @@ package kr.touroot.travelogue.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; public record TraveloguePositionRequest( @Schema(description = "여행기 장소 위도", example = "37.5175896") - @NotNull(message = "여행기 장소 위도는 비어있을 수 없습니다.") + @NotBlank(message = "여행기 장소 위도는 비어있을 수 없습니다.") String lat, @Schema(description = "여행기 장소 경도", example = "127.0867236") - @NotNull(message = "여행기 장소 경도는 비어있을 수 없습니다.") + @NotBlank(message = "여행기 장소 경도는 비어있을 수 없습니다.") String lng ) { } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 623bac1e..8c0c4570 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -2,17 +2,20 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.util.List; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; public record TravelogueRequest( @Schema(description = "여행기 제목", example = "서울 강남 여행기") - @NotNull(message = "여행기 제목은 비어있을 수 없습니다.") + @NotBlank(message = "여행기 제목은 비어있을 수 없습니다.") + @Size(message = "여행기 제목은 20자를 초과할 수 없습니다.", max = 20) String title, @Schema(description = "여행기 섬네일", example = "https://thumbnail.png") - @NotNull(message = "여행기 섬네일은 비어있을 수 없습니다.") + @NotBlank(message = "여행기 섬네일은 비어있을 수 없습니다.") String thumbnail, @Schema(description = "여행기 일자 목록") @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 7779c3a9..5d56ed08 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -1,21 +1,28 @@ package kr.touroot.travelplan.domain; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; - @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TravelPlan extends BaseEntity { + private static final int TITLE_MIN_LENGTH = 1; + private static final int TITLE_MAX_LENGTH = 20; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,14 +37,49 @@ public class TravelPlan extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private Member author; - public TravelPlan(String title, LocalDate startDate) { - this(null, title, startDate, null); + public TravelPlan(Long id, String title, LocalDate startDate, Member author) { + validate(title, startDate, author); + this.id = id; + this.title = title; + this.startDate = startDate; + this.author = author; } public TravelPlan(String title, LocalDate startDate, Member author) { this(null, title, startDate, author); } + + private void validate(String title, LocalDate startDate, Member author) { + validateNotNull(title, startDate, author); + validateNotBlank(title); + validateTitleLength(title); + } + + private void validateNotNull(String title, LocalDate startDate, Member author) { + if (title == null || startDate == null || author == null) { + throw new BadRequestException("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); + } + } + + private void validateNotBlank(String title) { + if (title.isBlank()) { + throw new BadRequestException("여행 계획에서 제목은 공백 문자로만 이루어질 수 없습니다"); + } + } + + private void validateTitleLength(String title) { + if (TITLE_MIN_LENGTH > title.length() || title.length() > TITLE_MAX_LENGTH) { + throw new BadRequestException("여행 계획은 " + TITLE_MIN_LENGTH + "자 이상, " + TITLE_MAX_LENGTH + "자 이하여야 합니다"); + } + } + + public void validateStartDate() { + if (startDate.isBefore(LocalDate.now())) { + throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); + } + } + public boolean isValidStartDate() { return startDate.isAfter(LocalDate.now()); } diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java index fcfaa349..3ed5aa21 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java @@ -10,14 +10,13 @@ import jakarta.persistence.ManyToOne; import java.time.LocalDate; import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TravelPlanDay extends BaseEntity { @@ -25,17 +24,41 @@ public class TravelPlanDay extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "plan_day_order", nullable = false) + @Column(name = "PLAN_DAY_ORDER", nullable = false) Integer order; - @JoinColumn(name = "plan_id", nullable = false) + @JoinColumn(name = "PLAN_ID", nullable = false) @ManyToOne(fetch = FetchType.LAZY) private TravelPlan plan; - public TravelPlanDay(int order, TravelPlan plan) { + private TravelPlanDay(Long id, Integer order, TravelPlan plan) { + validate(order, plan); + this.id = id; + this.order = order; + this.plan = plan; + } + + public TravelPlanDay(Integer order, TravelPlan plan) { this(null, order, plan); } + private void validate(Integer order, TravelPlan plan) { + validateNotNull(order, plan); + validateOrderRange(order); + } + + private void validateNotNull(Integer order, TravelPlan plan) { + if (order == null || plan == null) { + throw new BadRequestException("여행 계획 날짜에서 순서와 속하고 있는 여행 계획은 비어 있을 수 없습니다"); + } + } + + private void validateOrderRange(Integer order) { + if (order < 0) { + throw new BadRequestException("여행 계획 날짜 순서는 음수일 수 없습니다"); + } + } + public LocalDate getCurrentDate() { LocalDate startDate = plan.getStartDate(); return startDate.plusDays(order); diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index d69b4d4f..2ceeb606 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -8,37 +8,71 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import kr.touroot.global.entity.BaseEntity; -import kr.touroot.place.domain.Place; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity public class TravelPlanPlace extends BaseEntity { + private static final int MAX_DESCRIPTION_LENGTH = 300; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String description; - @Column(name = "plan_place_order", nullable = false) + @Column(name = "PLAN_PLACE_ORDER", nullable = false) private Integer order; - @JoinColumn(name = "plan_day_id", nullable = false) + @JoinColumn(name = "PLAN_DAY_ID", nullable = false) @ManyToOne(fetch = FetchType.LAZY) private TravelPlanDay day; - @JoinColumn(name = "place_id", nullable = false) + @JoinColumn(name = "PLACE_ID", nullable = false) @ManyToOne(fetch = FetchType.LAZY) private Place place; - public TravelPlanPlace(String description, int order, TravelPlanDay day, Place place) { + private TravelPlanPlace(Long id, String description, Integer order, TravelPlanDay day, Place place) { + validate(description, order, day, place); + this.id = id; + this.description = description; + this.order = order; + this.day = day; + this.place = place; + } + + public TravelPlanPlace(String description, Integer order, TravelPlanDay day, Place place) { this(null, description, order, day, place); } + + private void validate(String description, Integer order, TravelPlanDay day, Place place) { + validateNotNull(order, day, place); + validateDescriptionLength(description); + validateOrderRange(order); + } + + private void validateNotNull(Integer order, TravelPlanDay day, Place place) { + if (order == null || day == null || place == null) { + throw new BadRequestException("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); + } + } + + private void validateDescriptionLength(String description) { + if (description != null && description.length() > MAX_DESCRIPTION_LENGTH) { + throw new BadRequestException("장소 설명은 " + MAX_DESCRIPTION_LENGTH + "자를 넘을 수 없습니다"); + } + } + + private void validateOrderRange(Integer order) { + if (order < 0) { + throw new BadRequestException("장소의 방문 순서는 음수일 수 없습니다"); + } + } } diff --git a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java index 9fef0233..f5064936 100644 --- a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java @@ -1,9 +1,16 @@ package kr.touroot.authentication.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.fasterxml.jackson.databind.ObjectMapper; import kr.touroot.authentication.dto.response.LoginResponse; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.authentication.service.LoginService; -import org.junit.jupiter.api.Disabled; +import kr.touroot.global.auth.JwtAuthFilter; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -13,13 +20,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@Disabled @WebMvcTest(LoginController.class) class LoginControllerTest { @@ -27,10 +27,14 @@ class LoginControllerTest { private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; + @Autowired + private JwtAuthFilter jwtAuthFilter; @MockBean private LoginService loginService; @MockBean - JpaMetamodelMappingContext jpaMetamodelMappingContext; + private JpaMetamodelMappingContext jpaMetamodelMappingContext; + @MockBean + private JwtTokenProvider jwtTokenProvider; @DisplayName("카카오 로그인 요청을 처리할 수 있다") @Test @@ -38,7 +42,7 @@ void loginTest() throws Exception { LoginResponse loginResponse = new LoginResponse("리비", "img-url", "test-access-token"); when(loginService.login(any(String.class))).thenReturn(loginResponse); - mockMvc.perform(post("/api/v1/login/oauth/kakao") + mockMvc.perform(get("/api/v1/login/oauth/kakao") .param("code", "test-authorization-code")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) diff --git a/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java index f03a1c6b..f6e24691 100644 --- a/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java +++ b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java @@ -4,5 +4,5 @@ public class MemberFixture { - public static final Member MEMBER_1 = new Member(1L, 1L, "리비", "image-url"); + public static final Member MEMBER_1 = new Member(1L, "리비", "http://imageurl.com"); } diff --git a/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java b/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java index 308f3758..dbcfeba6 100644 --- a/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java +++ b/backend/src/test/java/kr/touroot/authentication/fixture/OauthUserInformationFixture.java @@ -7,6 +7,6 @@ public class OauthUserInformationFixture { public static final OauthUserInformationResponse USER_1_OAUTH_INFORMATION = new OauthUserInformationResponse( - 1L, new KakaoAccount(new KakaoProfile("리비", "img-url")) + 1L, new KakaoAccount(new KakaoProfile("리비", "http://img-url.com")) ); } diff --git a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java index 850e20b7..ef7da55a 100644 --- a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java +++ b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java @@ -14,20 +14,19 @@ class MemberTest { private static final Long VALID_SOCIAl_ID = 1L; private static final String VALID_NICKNAME = "nickname"; - private static final String VALID_PROFILE_IMAGE_URL = "url"; - + private static final String VALID_PROFILE_IMAGE_URL = "http://touroot.kr/images/1"; @DisplayName("검증 규칙을 통과하는 멤버 생성은 예외가 발생하지 않는다") @Test void createMemberWithValidData() { - assertThatCode(() -> new Member(1L, VALID_NICKNAME, "url")) + assertThatCode(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL)) .doesNotThrowAnyException(); } @DisplayName("카카오 아이디가 null일 때 멤버 생성 시 예외가 발생한다") @Test void createMemberWithKakaoIdNull() { - assertThatThrownBy(() -> new Member(null, VALID_NICKNAME, "url")) + assertThatThrownBy(() -> new Member(null, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL)) .isInstanceOf(BadRequestException.class) .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -35,7 +34,7 @@ void createMemberWithKakaoIdNull() { @DisplayName("닉네임이 null인 경우 멤버 생성 시 예외가 발생한다") @Test void createMemberWithNicknameNull() { - assertThatThrownBy(() -> new Member(1L, null, "url")) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, null, VALID_PROFILE_IMAGE_URL)) .isInstanceOf(BadRequestException.class) .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -43,7 +42,7 @@ void createMemberWithNicknameNull() { @DisplayName("프로필 이미지 경로가 null일 경우 멤버 생성 시 예외가 발생한다") @Test void createMemberWithProfileImageUrlNull() { - assertThatThrownBy(() -> new Member(1L, VALID_NICKNAME, null)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, null)) .isInstanceOf(BadRequestException.class) .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -52,7 +51,7 @@ void createMemberWithProfileImageUrlNull() { @ParameterizedTest @ValueSource(strings = {"", " "}) void createMemberWithBlankNickname(String blankNickname) { - assertThatThrownBy(() -> new Member(1L, blankNickname, "url")) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, blankNickname, VALID_PROFILE_IMAGE_URL)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -61,7 +60,7 @@ void createMemberWithBlankNickname(String blankNickname) { @ParameterizedTest @ValueSource(strings = {"", " "}) void createMemberWithProfileImageBlank(String blankUrl) { - assertThatThrownBy(() -> new Member(1L, VALID_NICKNAME, blankUrl)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, blankUrl)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -70,8 +69,17 @@ void createMemberWithProfileImageBlank(String blankUrl) { @ParameterizedTest @ValueSource(strings = {"21-length-nicknameeee", "22-length-nicknameeeee"}) void createMemberWithInvalidLengthNickname(String invalidLengthNickname) { - assertThatThrownBy(() -> new Member(1L, invalidLengthNickname, "url")) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, invalidLengthNickname, VALID_PROFILE_IMAGE_URL)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임은 1자 이상, 20자 이하여야 합니다"); } + + @DisplayName("프로필 이미지 url의 형식이 잘못된 경우 멤버 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"htt:touroot.kr", "touroot.kr"}) + void createMemberWithInvalidProfileImageUrl(String invalidProfileImageUrl) { + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, invalidProfileImageUrl)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미지 url 형식이 잘못되었습니다"); + } } diff --git a/backend/src/test/java/kr/touroot/place/domain/PlaceTest.java b/backend/src/test/java/kr/touroot/place/domain/PlaceTest.java new file mode 100644 index 00000000..3bbf3b35 --- /dev/null +++ b/backend/src/test/java/kr/touroot/place/domain/PlaceTest.java @@ -0,0 +1,104 @@ +package kr.touroot.place.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("장소") +class PlaceTest { + + private static final String VALID_PLACE_NAME = "유효한 장소 이름"; + private static final String VALID_LATITUDE = "37.5175896"; + private static final String VALID_LONGITUDE = "127.0867236"; + private static final String VALID_GOOGLE_PLACE_ID = "ChIJgUbEo8cfqokR5lP9_Wh_DaM"; + + @DisplayName("유효한 장소를 생성하는 경우 생성 시 예외가 발생하지 않는다") + @Test + void createPlaceWithValidData() { + assertThatCode(() -> new Place(VALID_PLACE_NAME, VALID_LATITUDE, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .doesNotThrowAnyException(); + } + + @DisplayName("장소 이름이 null인 경우 장소 생성 시 예외가 발생한다") + @Test + void createPlaceWithNullPlaceName() { + assertThatThrownBy(() -> new Place(null, VALID_LATITUDE, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + + @DisplayName("장소 이름의 길이가 60자를 초과하는 경우 장소 생성 시 예외가 발생한다") + @Test + void createPlaceWithInvalidLengthPlaceName() { + String length61 = "Under the summer sun, feeling the cool breeze by the sea is pure joy!!"; + assertThatThrownBy(() -> new Place(length61, VALID_LATITUDE, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름은 60자 이하여야 합니다"); + } + + @DisplayName("위도가 null인 경우 장소 생성 시 예외가 발생한다") + @Test + void createPlaceWithNullLatitude() { + assertThatThrownBy(() -> new Place(VALID_PLACE_NAME, null, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + + @DisplayName("경도가 null인 경우 장소 생성 시 예외가 발생한다") + @Test + void createPlaceWithNullLongitude() { + assertThatThrownBy(() -> new Place(VALID_PLACE_NAME, VALID_LATITUDE, null, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + + @DisplayName("장소 이름이 비어 있는 경우 장소 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {" ", " "}) + void createPlaceWithBlankPlaceName(String blank) { + assertThatThrownBy(() -> new Place(blank, VALID_LATITUDE, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + + @DisplayName("위도가 비어 있는 경우 장소 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {" ", " "}) + void createPlaceWithBlankLatitude(String blank) { + assertThatThrownBy(() -> new Place(VALID_PLACE_NAME, blank, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + + @DisplayName("경도가 비어 있는 경우 장소 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {" ", " "}) + void createPlaceWithBlankLongitude(String blank) { + assertThatThrownBy(() -> new Place(VALID_PLACE_NAME, VALID_LATITUDE, blank, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 이름, 위도, 경도는 비어 있을 수 없습니다"); + } + + @DisplayName("위도 형식에 맞지 않는 위도로 장소 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"91.0000", "-91.0000", "100.0000", "-100.0000", "abc", "45.0000.0", "45,0000"}) + void createPlaceWithMalformedLatitude(String malformed) { + assertThatThrownBy(() -> new Place(VALID_PLACE_NAME, malformed, VALID_LONGITUDE, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("위,경도의 형식이 올바르지 않습니다"); + } + + @DisplayName("경도 형식에 맞지 않는 위도로 장소 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"181.0000", "-181.0000", "200.0000", "-200.0000", "abc", "100.0000.0", "100,0000"}) + void createPlaceWithMalformedLongitude(String malformed) { + assertThatThrownBy(() -> new Place(VALID_PLACE_NAME, VALID_LATITUDE, malformed, VALID_GOOGLE_PLACE_ID)) + .isInstanceOf(BadRequestException.class) + .hasMessage("위,경도의 형식이 올바르지 않습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java new file mode 100644 index 00000000..50139a66 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java @@ -0,0 +1,50 @@ +package kr.touroot.travelogue.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.fixture.TravelogueFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행기 날짜") +class TravelogueDayTest { + + private static final Integer VALID_ORDER = 0; + private static final Travelogue VALID_TRAVELOGUE = TravelogueFixture.TRAVELOGUE.get(); + + @DisplayName("유효한 여행 날짜 생성 시 예외가 발생하지 않는다") + @Test + void createTravelogueDayWithValidData() { + assertThatCode(() -> new TravelogueDay(VALID_ORDER, VALID_TRAVELOGUE)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행 날짜의 순서는 비어 있을 수 없다") + @Test + void createTravelogueDayWithNullOrder() { + assertThatThrownBy(() -> new TravelogueDay(null, VALID_TRAVELOGUE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 날짜가 속한 여행기와 여행 날짜의 순서는 비어 있을 수 없습니다"); + } + + @DisplayName("여행 날짜가 속할 여행기는 비어 있을 수 없다") + @Test + void createTravelogueDayWithNullTravelogue() { + assertThatThrownBy(() -> new TravelogueDay(VALID_ORDER, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 날짜가 속한 여행기와 여행 날짜의 순서는 비어 있을 수 없습니다"); + } + + @DisplayName("여행 날짜의 순서는 음수가 될 수 없다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, -3, -4, -5}) + void createTravelogueDayWithNegativeOrder(int negative) { + assertThatThrownBy(() -> new TravelogueDay(negative, VALID_TRAVELOGUE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 날짜의 순서는 음수 일 수 없습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePhotoTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePhotoTest.java new file mode 100644 index 00000000..354808e7 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePhotoTest.java @@ -0,0 +1,59 @@ +package kr.touroot.travelogue.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelogue.fixture.TraveloguePlaceFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행 장소의 사진") +class TraveloguePhotoTest { + + private static final Integer VALID_ORDER = 0; + private static final String VALID_KEY = "valid-key"; + private static final TraveloguePlace VALID_TRAVELOGUE_PLACE = TraveloguePlaceFixture.TRAVELOGUE_PLACE.get(); + + @DisplayName("올바른 여행 장소 사진을 생성 시에는 예외가 발생하지 않는다") + @Test + void createTraveloguePhotoWithValidData() { + assertThatCode(() -> new TraveloguePhoto(VALID_ORDER, VALID_KEY, VALID_TRAVELOGUE_PLACE)) + .doesNotThrowAnyException(); + } + + @DisplayName("사진의 순서가 비어 있는 경우 사진 생성 시 예외가 발생한다") + @Test + void createTraveloguePhotoWithNullOrder() { + assertThatThrownBy(() -> new TraveloguePhoto(null, VALID_KEY, VALID_TRAVELOGUE_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 장소의 사진에서 순서와 키, 그리고 사진이 속한 여행 장소는 비어 있을 수 없습니다"); + } + + @DisplayName("사진의 장소가 비어 있는 경우 사진 생성 시 예외가 발생한다") + @Test + void createTraveloguePhotoWithNullPlace() { + assertThatThrownBy(() -> new TraveloguePhoto(VALID_ORDER, VALID_KEY, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 장소의 사진에서 순서와 키, 그리고 사진이 속한 여행 장소는 비어 있을 수 없습니다"); + } + + @DisplayName("사진의 키값이 비어 있는 경우 사진 생성 시 예외가 발생한다") + @Test + void createTraveloguePhotoWithNullKey() { + assertThatThrownBy(() -> new TraveloguePhoto(VALID_ORDER, null, VALID_TRAVELOGUE_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 장소의 사진에서 순서와 키, 그리고 사진이 속한 여행 장소는 비어 있을 수 없습니다"); + } + + @DisplayName("사진의 순서가 음수인 경우 사진 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, -3, -4, -5}) + void createTraveloguePhotoWithNegativeOrder(int negative) { + assertThatThrownBy(() -> new TraveloguePhoto(negative, VALID_KEY, VALID_TRAVELOGUE_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 장소에 속하는 사진의 순서는 음수가 될 수 없습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java new file mode 100644 index 00000000..7ade1d18 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TraveloguePlaceTest.java @@ -0,0 +1,80 @@ +package kr.touroot.travelogue.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; +import kr.touroot.place.fixture.PlaceFixture; +import kr.touroot.travelogue.fixture.TravelogueDayFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행기 장소") +class TraveloguePlaceTest { + + private static final Integer VALID_ORDER = 0; + private static final Place VALID_PLACE = PlaceFixture.PLACE.get(); + private static final TravelogueDay VALID_DAY = TravelogueDayFixture.TRAVELOGUE_DAY.get(); + private static final String VALID_DESCRIPTION = "장소에 대한 설명"; + + @DisplayName("올바른 여행기 장소 생성 시 예외가 발생하지 않는다") + @Test + void createTraveloguePlaceWithValidData() { + assertThatCode(() -> new TraveloguePlace(VALID_ORDER, VALID_DESCRIPTION, VALID_PLACE, VALID_DAY)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행기 장소 생성 시 순서가 비어 있다면 예외가 발생한다") + @Test + void createTraveloguePlaceWithNullOrder() { + assertThatThrownBy(() -> new TraveloguePlace(null, VALID_DESCRIPTION, VALID_PLACE, VALID_DAY)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 장소에서 순서와 장소 상세 정보, 그리고 방문 날짜는 비어 있을 수 없습니다"); + } + + @DisplayName("여행기 장소 생성 시 장소 상세 정보가 비어 있다면 예외가 발생한다") + @Test + void createTraveloguePlaceWithNullPlace() { + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESCRIPTION, null, VALID_DAY)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 장소에서 순서와 장소 상세 정보, 그리고 방문 날짜는 비어 있을 수 없습니다"); + } + + @DisplayName("여행기 장소 생성 시 장소가 속한 날짜가 비어 있다면 예외가 발생한다") + @Test + void createTraveloguePlaceWithNullDay() { + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, VALID_DESCRIPTION, VALID_PLACE, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 장소에서 순서와 장소 상세 정보, 그리고 방문 날짜는 비어 있을 수 없습니다"); + } + + @DisplayName("여행기 장소 생성 시 장소 설명이 비어 있더라도 여행기를 생성할 수 있다") + @Test + void createTraveloguePlaceWithNullDescription() { + assertThatCode(() -> new TraveloguePlace(VALID_ORDER, null, VALID_PLACE, VALID_DAY)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행기 장소의 순서는 음수 일 수 없다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, -3, -4, -5}) + void createTraveloguePlaceWithNegativeOrder(int negative) { + assertThatThrownBy(() -> new TraveloguePlace(negative, VALID_DESCRIPTION, VALID_PLACE, VALID_DAY)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 장소의 순서는 음수일 수 없습니다"); + } + + @DisplayName("여행기 장소 설명의 길이가 300자가 넘는 경우 여행기 생성 시 예외가 발생한다") + @Test + void createTraveloguePlaceWithInvalidLengthDescription() { + String length301Description = "서울의 명동은 현대와 전통이 조화롭게 어우러진 매력적인 지역입니다. 이곳의 거리에는 최신 패션 아이템을 갖춘 상점들이 즐비하며, 각종 뷰티 제품을 직접 체험할 수 있는 기회가 많습니다. 다양한 길거리 음식과 맛집이 가득해 미식가들의 입맛을 사로잡습니다. 서울타워와 N서울타워 전망대에서는 서울 전경을 한눈에 감상할 수 있으며, 남산 공원에서는 도심 속의 자연을 즐길 수 있습니다. 전통 시장인 남대문 시장과 청계천은 서울의 풍부한 역사와 문화를 체험할 수 있는 명소입니다. 명동의 활기 넘치는 분위기 속에서 쇼핑과 먹거리를 동시에 즐겨요!"; + + System.out.println("length301Description = " + length301Description.length()); + assertThatThrownBy(() -> new TraveloguePlace(VALID_ORDER, length301Description, VALID_PLACE, VALID_DAY)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 장소에 대한 설명은 300자를 넘길 수 없습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java new file mode 100644 index 00000000..73ff341f --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java @@ -0,0 +1,87 @@ +package kr.touroot.travelogue.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.authentication.fixture.MemberFixture; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행기 도메인") +class TravelogueTest { + + private static final Member VALID_AUTHOR = MemberFixture.MEMBER_1; + private static final String VALID_TITLE = "올바른 여행기 제목"; + private static final String VALID_THUMBNAIL = "http://valid-thumbnail.com"; + + @DisplayName("검증 규칙에 어긋나지 않는 여행기 생성 시 예외가 발생하지 않는다") + @Test + void createTravelogueWithValidData() { + assertThatCode(() -> new Travelogue(VALID_AUTHOR, VALID_TITLE, VALID_THUMBNAIL)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행기 제목이 null인 경우 여행기 생성 시 예외가 발생한다") + @Test + void createTravelogueWithNullTitle() { + assertThatThrownBy(() -> new Travelogue(VALID_AUTHOR, null, VALID_THUMBNAIL)) + .isInstanceOf(BadRequestException.class) + .hasMessage("작성자, 여행기 제목, 그리고 여행기 썸네일은 비어있을 수 없습니다"); + } + + @DisplayName("여행기 작성자가 null인 경우 여행기 생성 시 예외가 발생한다") + @Test + void createTravelogueWithNullAuthor() { + assertThatThrownBy(() -> new Travelogue(null, VALID_TITLE, VALID_THUMBNAIL)) + .isInstanceOf(BadRequestException.class) + .hasMessage("작성자, 여행기 제목, 그리고 여행기 썸네일은 비어있을 수 없습니다"); + } + + @DisplayName("여행기 썸네일이 null인 경우 여행기 생성 시 예외가 발생한다") + @Test + void createTravelogueWithNullThumbNail() { + assertThatThrownBy(() -> new Travelogue(VALID_AUTHOR, VALID_TITLE, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("작성자, 여행기 제목, 그리고 여행기 썸네일은 비어있을 수 없습니다"); + } + + @DisplayName("여행기 제목이 비어 있는 경우 여행기 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", " ", " "}) + void createTravelogueWithBlankTitle(String blank) { + assertThatThrownBy(() -> new Travelogue(VALID_AUTHOR, blank, VALID_THUMBNAIL)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 제목, 여행기 썸네일은 비어있을 수 없습니다"); + } + + @DisplayName("여행기 썸네일이 비어 있는 경우 여행기 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", " ", " "}) + void createTravelogueWithBlankThumbNail(String blank) { + assertThatThrownBy(() -> new Travelogue(VALID_AUTHOR, VALID_TITLE, blank)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 제목, 여행기 썸네일은 비어있을 수 없습니다"); + } + + @DisplayName("여행기 제목의 길이가 1자 이상 20자 이하가 아닌 경우 여행기 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"21-length-stringggggg", "22-length-stringgggggg", "23-length-stringggggggg"}) + void createTravelogueWithInvalidLengthTitle(String invalidLengthTitle) { + assertThatThrownBy(() -> new Travelogue(VALID_AUTHOR, invalidLengthTitle, VALID_THUMBNAIL)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행기 제목은 1자 이상, 20자 이하여야 합니다"); + } + + @DisplayName("여행기 썸네일 경로가 URL형식을 벗어나는 경우 여행기 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"ht://touroot.com/images/1", "touroot.com/images/1"}) + void createTravelogueWithInvalidThumbnail(String invalidThumbnail) { + assertThatThrownBy(() -> new Travelogue(VALID_AUTHOR, VALID_TITLE, invalidThumbnail)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미지 url 형식이 잘못되었습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 00519acd..a3188b66 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -95,7 +95,7 @@ public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { } public Member initMemberTestData() { - Member member = new Member(1L, "tester", "image"); + Member member = new Member(1L, "tester", "http://image.com"); return memberRepository.save(member); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanDayTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanDayTest.java new file mode 100644 index 00000000..4f0fc954 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanDayTest.java @@ -0,0 +1,50 @@ +package kr.touroot.travelplan.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelplan.fixture.TravelPlanFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행 계획 날짜") +class TravelPlanDayTest { + + private static final Integer VALID_ORDER = 0; + private static final TravelPlan VALID_PLAN = TravelPlanFixture.TRAVEL_PLAN.get(); + + @DisplayName("올바른 여행 계획 날짜 생성 시 예외가 발생하지 않는다") + @Test + void createTravelPlanDayWithValidData() { + assertThatCode(() -> new TravelPlanDay(VALID_ORDER, VALID_PLAN)) + .doesNotThrowAnyException(); + } + + @DisplayName("날짜의 순서가 비어 있을 경우 여행 계획 날짜 생성 시 예외가 발생한다") + @Test + void createTravelPlanDayWithNullOrder() { + assertThatThrownBy(() -> new TravelPlanDay(null, VALID_PLAN)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 날짜에서 순서와 속하고 있는 여행 계획은 비어 있을 수 없습니다"); + } + + @DisplayName("날짜가 속한 여행 계획이 비어 있을 경우 여행 계획 날짜 생성 시 예외가 발생한다") + @Test + void createTravelPlanDayWithNullPlan() { + assertThatThrownBy(() -> new TravelPlanDay(VALID_ORDER, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 날짜에서 순서와 속하고 있는 여행 계획은 비어 있을 수 없습니다"); + } + + @DisplayName("여행 계획 날짜의 순서가 음수인 경우 여행 계획 날짜 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, -3, -4, -5}) + void createTravelPlanDayWithNegativeOrder(int negative) { + assertThatThrownBy(() -> new TravelPlanDay(negative, VALID_PLAN)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 날짜 순서는 음수일 수 없습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java new file mode 100644 index 00000000..e7c1a392 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java @@ -0,0 +1,79 @@ +package kr.touroot.travelplan.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.place.domain.Place; +import kr.touroot.place.fixture.PlaceFixture; +import kr.touroot.travelplan.fixture.TravelPlanDayFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행 계획 장소") +class TravelPlanPlaceTest { + + private static final String VALID_DESCRIPTION = "함덕에서 유명한 맛집"; + private static final Integer VALID_ORDER = 0; + private static final TravelPlanDay VALID_DAY = TravelPlanDayFixture.TRAVEL_PLAN_DAY.get(); + private static final Place VALID_PLACE = PlaceFixture.PLACE.get(); + + @DisplayName("올바른 여행 계획 장소 생성 시 예외가 발생하지 않는다") + @Test + void createTravelPlanPlaceWithValidData() { + assertThatCode(() -> new TravelPlanPlace(VALID_DESCRIPTION, VALID_ORDER, VALID_DAY, VALID_PLACE)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행 계획 장소의 설명이 비어 있어도 예외가 발생하지 않는다") + @Test + void createTravelPlanPlaceWithNullDescription() { + assertThatCode(() -> new TravelPlanPlace(null, VALID_ORDER, VALID_DAY, VALID_PLACE)) + .doesNotThrowAnyException(); + } + + @DisplayName("방문 순서가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") + @Test + void createTravelPlanPlaceWithNullOrder() { + assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, null, VALID_DAY, VALID_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); + } + + @DisplayName("장소의 방문 날짜가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") + @Test + void createTravelPlanPlaceWithNullDay() { + assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, VALID_ORDER, null, VALID_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); + } + + @DisplayName("장소 상세 정보가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") + @Test + void createTravelPlanPlaceWithPlaceNull() { + assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, VALID_ORDER, VALID_DAY, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); + } + + @DisplayName("여행 계획 장소 설명이 300자를 넘는 경우 여행 계획 장소 생성 시 예외가 발생한다") + @Test + void createTravelPlanPlaceWithTitleLengthOver300() { + String length301 = "함덕 해수욕장은 제주도의 아름다운 해변으로, 맑고 푸른 바다와 깨끗한 백사장이 특징입니다. 이곳은 특히 여름철에 인기가 높으며, 가족 단위 방문객과 커플 모두에게 적합한 장소입니다. 해변의 파도는 비교적 잔잔하여 수영이나 서핑을 즐기기에 좋고, 주변에는 다양한 해산물 맛집과 카페들이 있어 식사와 음료를 즐기기에 편리합니다. 해변을 따라 펼쳐진 산책로에서는 제주도의 자연경관을 감상하며 여유로운 산책이 가능합니다. 특히 일몰 시의 경치가 아름다워 사진 찍기에도 최적의 장소입니다. 함덕 해수욕장에서 제주도의 매력을 만끽해 보세요!!!!!!"; + + assertThatThrownBy(() -> new TravelPlanPlace(length301, VALID_ORDER, VALID_DAY, VALID_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소 설명은 300자를 넘을 수 없습니다"); + } + + @DisplayName("여행 계획 장소의 방문 순서가 음수인 경우 여행 계획 장소 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, -3, -4, -5}) + void createTravelPlanPlaceWithNegativeOrder(int negative) { + assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, negative, VALID_DAY, VALID_PLACE)) + .isInstanceOf(BadRequestException.class) + .hasMessage("장소의 방문 순서는 음수일 수 없습니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index da4bb014..407af5b9 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -1,22 +1,75 @@ package kr.touroot.travelplan.domain; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import kr.touroot.authentication.fixture.MemberFixture; +import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; - -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; @DisplayName("여행 계획") class TravelPlanTest { + private static final Member VALID_AUTHOR = MemberFixture.MEMBER_1; + private static final String VALID_TITLE = "제주도 여행 계획"; + private static final LocalDate VALID_START_DATE = LocalDate.now().plusDays(2); + + @DisplayName("올바른 여행 계획 생성 시에는 예외가 발생하지 않는다") + @Test + void createTravelPlanWithValidData() { + assertThatCode(() -> new TravelPlan(VALID_TITLE, VALID_START_DATE, VALID_AUTHOR)) + .doesNotThrowAnyException(); + } + + @DisplayName("여행 계획의 제목이 비어 있는 경우 여행 계획 생성 시 예외가 발생한다") + @Test + void createTravelPlanWithNullTitle() { + assertThatThrownBy(() -> new TravelPlan(null, VALID_START_DATE, VALID_AUTHOR)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); + } + + @DisplayName("여행 계획의 시작 날짜가 비어 있는 경우 여행 계획 생성 시 예외가 발생한다") + @Test + void createTravelPlanWithNullStartDate() { + assertThatThrownBy(() -> new TravelPlan(VALID_TITLE, null, VALID_AUTHOR)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); + } + + @DisplayName("여행 계획 제목이 공백 문자로만 이루어져 있는 경우 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {" ", " ", " "}) + void createTravelPlanWithBlankTitle(String blank) { + assertThatThrownBy(() -> new TravelPlan(blank, VALID_START_DATE, VALID_AUTHOR)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획에서 제목은 공백 문자로만 이루어질 수 없습니다"); + } + + @DisplayName("여행 계획 제목이 1자에서 20자 사이의 길이가 아니라면 생성 시 예외가 발생한다") + @Test + void createTravelPlanWithInvalidLengthTitle() { + String length21 = "서울 명동: 패션 쇼핑과 길거리 음식,"; + + assertThatThrownBy(() -> new TravelPlan(length21, VALID_START_DATE, VALID_AUTHOR)) + .isInstanceOf(BadRequestException.class) + .hasMessage("여행 계획은 1자 이상, 20자 이하여야 합니다"); + } + @DisplayName("여행 계획은 지난 날짜를 검증할 시 예외가 발생한다.") @Test void validateStartDate() { - // given - TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN); + TravelPlan travelPlan = new TravelPlan(VALID_TITLE, LocalDate.MIN, VALID_AUTHOR); + assertThatCode(travelPlan::validateStartDate) + .isInstanceOf(BadRequestException.class) + .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); // when boolean actual = travelPlan.isValidStartDate(); diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java new file mode 100644 index 00000000..1d9327c1 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java @@ -0,0 +1,23 @@ +package kr.touroot.travelplan.fixture; + +import static kr.touroot.travelplan.fixture.TravelPlanFixture.TRAVEL_PLAN; + +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.domain.TravelPlanDay; + +public enum TravelPlanDayFixture { + + TRAVEL_PLAN_DAY(0, TRAVEL_PLAN.get()); + + private final int order; + private final TravelPlan travelPlan; + + TravelPlanDayFixture(int order, TravelPlan travelPlan) { + this.order = order; + this.travelPlan = travelPlan; + } + + public TravelPlanDay get() { + return new TravelPlanDay(order, travelPlan); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java new file mode 100644 index 00000000..1ee228cc --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java @@ -0,0 +1,25 @@ +package kr.touroot.travelplan.fixture; + +import java.time.LocalDate; +import kr.touroot.authentication.fixture.MemberFixture; +import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlan; + +public enum TravelPlanFixture { + + TRAVEL_PLAN("제주도 여행 계획", LocalDate.now().plusDays(2), MemberFixture.MEMBER_1); + + private final String title; + private final LocalDate startDate; + private final Member author; + + TravelPlanFixture(String title, LocalDate startDate, Member author) { + this.title = title; + this.startDate = startDate; + this.author = author; + } + + public TravelPlan get() { + return new TravelPlan(title, startDate, author); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 8394b998..3e8e7726 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -72,7 +72,7 @@ public long initTravelPlanTestData(Member author) { } public Member initMemberTestData() { - Member member = getMember(1L, "tester", "image"); + Member member = getMember(1L, "tester", "http://image.com"); return memberRepository.save(member); } } From 7b0f92c4b0525ecc2e2c4bd36cd5a441e7a6b14d Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:21:45 +0900 Subject: [PATCH 063/108] =?UTF-8?q?[Feature]=20-=20logback=20console=20app?= =?UTF-8?q?ender=EC=97=90=20=EC=83=89=EC=83=81=20=EC=B6=94=EA=B0=80=20(#18?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/logback-spring.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 1c1812ac..1e140d89 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -2,10 +2,11 @@ + - ${LOG_PATTERN} + ${CONSOLE_PATTERN} From d8c3dde265509aa909626859d00a9eb80f36ebee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:25:52 +0900 Subject: [PATCH 064/108] =?UTF-8?q?[Feature]=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20S3=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: uploadImages가 파일을 임시 폴더에 저장하도록 수정 * feat: 임시 폴더의 파일을 영구 폴더에 복사하는 기능 구현 * feat: 게시글 업로드 시 복사 메서드 호출하도록 기능 추가 - 새로운 url을 db에 저장하도록 수정 * fix: 잘못된 url prefix 수정 * test: url 검증 로직 추가로 인한 url 형식 수정 * test: S3 인증 로직 부분 Mock 처리 * style: 설정 변수명 변경 * style: 썸네일 철자 수정 * refactor: try 문 내부의 자원 관리를 try-with-resource 문으로 처리 --- .../image/infrastructure/AwsS3Provider.java | 53 +++++++++++--- .../dto/request/TraveloguePhotoRequest.java | 4 -- .../dto/request/TravelogueRequest.java | 4 +- .../dto/response/TravelogueResponse.java | 4 +- .../service/TraveloguePhotoService.java | 6 +- backend/src/main/resources/application.yml | 6 +- .../infrastructure/AwsS3ProviderTest.java | 72 +++++++++++++++++++ .../controller/TravelogueControllerTest.java | 13 +++- .../travelogue/fixture/TravelogueFixture.java | 2 +- .../fixture/TraveloguePhotoFixture.java | 2 +- .../fixture/TravelogueRequestFixture.java | 4 +- .../fixture/TravelogueResponseFixture.java | 6 +- .../service/TravelogueFacadeServiceTest.java | 24 +++++-- .../service/TraveloguePhotoServiceTest.java | 18 ++++- .../src/test/resources/application-test.yml | 6 +- 15 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java diff --git a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java index 98b8345f..7a33a121 100644 --- a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.domain.ImageFile; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -11,25 +13,31 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; import software.amazon.awssdk.services.s3.model.PutObjectRequest; -import kr.touroot.global.exception.BadRequestException; -import kr.touroot.image.domain.ImageFile; @Component public class AwsS3Provider { private final String bucket; private final String imageBaseUri; - private final String directoryPath; + private final String tourootStoragePath; + private final String temporaryStoragePath; + private final String imageStoragePath; public AwsS3Provider( @Value("${cloud.aws.s3.bucket}") String bucket, @Value("${cloud.aws.s3.image-base-uri}") String imageBaseUri, - @Value("${cloud.aws.s3.directory-path}") String directoryPath + @Value("${cloud.aws.s3.base-storage-path}") String tourootStoragePath, + @Value("${cloud.aws.s3.temporary-storage-path}") String temporaryStoragePath, + @Value("${cloud.aws.s3.image-storage-path}") String imageStoragePath ) { this.bucket = bucket; this.imageBaseUri = imageBaseUri; - this.directoryPath = directoryPath; + this.tourootStoragePath = tourootStoragePath; + this.temporaryStoragePath = temporaryStoragePath; + this.imageStoragePath = imageStoragePath; } public List uploadImages(List files) { @@ -40,9 +48,10 @@ public List uploadImages(List files) { .map(ImageFile::getFile) .forEach(file -> { String newFileName = createNewFileName(file.getOriginalFilename()); - String filePath = directoryPath + newFileName; + String filePath = tourootStoragePath + temporaryStoragePath + newFileName; uploadFile(file, filePath, s3Client); - urls.add(imageBaseUri + newFileName); + String s3Key = imageBaseUri + temporaryStoragePath + newFileName; + urls.add(s3Key); }); return urls; } @@ -52,7 +61,7 @@ private String createNewFileName(String fileName) { return UUID.randomUUID() + fileName.substring(fileName.lastIndexOf(".")); } - private S3Client getS3Client() { + S3Client getS3Client() { return S3Client.builder() .region(Region.AP_NORTHEAST_2) .credentialsProvider(InstanceProfileCredentialsProvider.create()) @@ -74,4 +83,32 @@ private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) throw new BadRequestException("파일 저장에 실패했습니다."); } } + + public String copyImageToPermanentStorage(String imageKey) { + validateS3Path(imageKey); + String destinationKey = imageKey.replace(temporaryStoragePath, imageStoragePath); + copyFile(imageKey, destinationKey); + return destinationKey; + } + + private void validateS3Path(String imageKey) { + if (!imageKey.startsWith(imageBaseUri + temporaryStoragePath)) { + throw new BadRequestException("이미지 url 형식이 잘못되었습니다."); + } + } + + private void copyFile(String sourceKey, String destinationKey) { + try (S3Client s3Client = getS3Client()) { + CopyObjectRequest request = CopyObjectRequest.builder() + .sourceBucket(bucket) + .sourceKey(sourceKey) + .destinationBucket(bucket) + .destinationKey(destinationKey) + .build(); + + s3Client.copyObject(request); + } catch (NoSuchKeyException e) { + throw new BadRequestException("복사하려는 사진이 존재하지 않습니다."); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java index b1e82852..1ad913df 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java @@ -10,8 +10,4 @@ public record TraveloguePhotoRequest( @NotBlank(message = "여행기 장소 사진 URL 값은 비어있을 수 없습니다.") String url ) { - - public TraveloguePhoto toTraveloguePhoto(int order, TraveloguePlace traveloguePlace) { - return new TraveloguePhoto(order, url, traveloguePlace); - } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 8c0c4570..16931bad 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -14,8 +14,8 @@ public record TravelogueRequest( @NotBlank(message = "여행기 제목은 비어있을 수 없습니다.") @Size(message = "여행기 제목은 20자를 초과할 수 없습니다.", max = 20) String title, - @Schema(description = "여행기 섬네일", example = "https://thumbnail.png") - @NotBlank(message = "여행기 섬네일은 비어있을 수 없습니다.") + @Schema(description = "여행기 썸네일", example = "https://thumbnail.png") + @NotNull(message = "여행기 썸네일은 비어있을 수 없습니다.") String thumbnail, @Schema(description = "여행기 일자 목록") @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index 07127a59..47d06d8f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -15,8 +15,8 @@ public record TravelogueResponse( @Schema(description = "여행기 제목", example = "서울 강남 여행기") @NotNull(message = "여행기 제목은 비어있을 수 없습니다.") String title, - @Schema(description = "여행기 섬네일 링크", example = "https://섬네일.png") - @NotNull(message = "여행기 섬네일 링크는 비어있을 수 없습니다.") + @Schema(description = "여행기 썸네일 링크", example = "https://썸네일.png") + @NotNull(message = "여행기 썸네일 링크는 비어있을 수 없습니다.") String thumbnail, @Schema(description = "여행기 일자 목록") @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java index 1ae68436..11b2d277 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java @@ -3,25 +3,29 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.s3.S3Client; @RequiredArgsConstructor @Service public class TraveloguePhotoService { private final TraveloguePhotoRepository traveloguePhotoRepository; + private final AwsS3Provider s3Provider; public List createPhotos(List requests, TraveloguePlace place) { List photos = new ArrayList<>(); for (int i = 0; i < requests.size(); i++) { TraveloguePhotoRequest request = requests.get(i); - TraveloguePhoto photo = request.toTraveloguePhoto(i, place); + String url = s3Provider.copyImageToPermanentStorage(request.url()); + TraveloguePhoto photo = new TraveloguePhoto(i, url, place); photos.add(traveloguePhotoRepository.save(photo)); } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e1ba01d4..85ce7c2f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -12,8 +12,10 @@ cloud: aws: s3: bucket: techcourse-project-2024 - image-base-uri: https://dev.touroot.kr/images/ - directory-path: touroot/images/ + image-base-uri: https://dev.touroot.kr/ + base-storage-path: touroot/ + temporary-storage-path: temporary/ + image-storage-path: images/ --- security: jwt: diff --git a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java new file mode 100644 index 00000000..c160a918 --- /dev/null +++ b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java @@ -0,0 +1,72 @@ +package kr.touroot.image.infrastructure; + +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import io.restassured.RestAssured; +import kr.touroot.authentication.fixture.OauthUserInformationFixture; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.global.exception.BadRequestException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CopyObjectRequest; +import software.amazon.awssdk.services.s3.model.CopyObjectResponse; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = AwsS3Provider.class) +class AwsS3ProviderTest { + + @MockBean + private S3Client s3Client; + + @SpyBean + private AwsS3Provider s3Provider; + + @Value("${cloud.aws.s3.image-base-uri}") + private String imageBaseUri; + @Value("${cloud.aws.s3.temporary-storage-path}") + private String temporaryStoragePath; + @Value("${cloud.aws.s3.image-storage-path}") + private String imageStoragePath; + + @DisplayName("유효한 url을 통해 이미지를 영구 폴더로 복사하면 새로운 url을 반환한다.") + @Test + void copyImageToPermanentStorage() { + when(s3Provider.getS3Client()) + .thenReturn(s3Client); + when(s3Client.copyObject(any(CopyObjectRequest.class))) + .thenReturn(CopyObjectResponse.builder().build()); + + String temporaryUrl = imageBaseUri + temporaryStoragePath + "valid.png"; + String imageUrl = imageBaseUri + imageStoragePath + "valid.png"; + assertThat(s3Provider.copyImageToPermanentStorage(temporaryUrl)) + .isEqualTo(imageUrl); + } + + @DisplayName("url이 올바른 버킷명과 폴더명으로 시작하지 않으면 예외를 발생한다.") + @Test + void copyImageToPermanentStorageWithInvalidPath() { + String imageUrl = "invalid/testUrl.png"; + assertThatThrownBy(() -> s3Provider.copyImageToPermanentStorage(imageUrl)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미지 url 형식이 잘못되었습니다."); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 7f1f7937..14f62cfb 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -1,6 +1,7 @@ package kr.touroot.travelogue.controller; import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -8,6 +9,7 @@ import io.restassured.http.ContentType; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; @@ -18,7 +20,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.data.domain.Page; import org.springframework.http.HttpHeaders; @@ -33,18 +37,22 @@ class TravelogueControllerTest { private final TravelogueTestHelper testHelper; private final ObjectMapper objectMapper; private final JwtTokenProvider jwtTokenProvider; + @MockBean + private final AwsS3Provider s3Provider; @Autowired public TravelogueControllerTest( DatabaseCleaner databaseCleaner, TravelogueTestHelper testHelper, ObjectMapper objectMapper, - JwtTokenProvider jwtTokenProvider + JwtTokenProvider jwtTokenProvider, + AwsS3Provider s3Provider ) { this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; this.objectMapper = objectMapper; this.jwtTokenProvider = jwtTokenProvider; + this.s3Provider = s3Provider; } @BeforeEach @@ -57,6 +65,9 @@ void setUp() { @DisplayName("여행기를 작성한다.") @Test void createTravelogue() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn("imageUrl.png"); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); Member member = testHelper.initMemberTestData(); String accessToken = jwtTokenProvider.createToken(member.getId()); diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java index 84b5de8a..979381ab 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java @@ -6,7 +6,7 @@ public enum TravelogueFixture { - TRAVELOGUE(MemberFixture.MEMBER_1, "제주에 하영 옵서", "https://url.com/jeju_thumbnail.png"); + TRAVELOGUE(MemberFixture.MEMBER_1, "제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png"); private final Member author; private final String title; diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java index e9e43d09..64c019db 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java @@ -7,7 +7,7 @@ public enum TraveloguePhotoFixture { - TRAVELOGUE_PHOTO(1, "https://image-url.com/image1.png", TRAVELOGUE_PLACE.get()); + TRAVELOGUE_PHOTO(1, "https://dev.touroot.kr/temporary/image1.png", TRAVELOGUE_PLACE.get()); private final int order; private final String url; diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index ffe53d0d..ef8cd2cb 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -15,7 +15,7 @@ private TravelogueRequestFixture() { } public static TravelogueRequest getTravelogueRequest() { - return new TravelogueRequest("제주에 하영 옵서", "https://url.com/jeju_thumbnail.png", getTravelogueDayRequests()); + return new TravelogueRequest("제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png", getTravelogueDayRequests()); } public static List getTravelogueDayRequests() { @@ -36,6 +36,6 @@ public static TraveloguePositionRequest getTraveloguePositionRequest() { } public static List getTraveloguePhotoRequests() { - return List.of(new TraveloguePhotoRequest("https://image-url.com/image1.png")); + return List.of(new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image1.png")); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 3334e397..3b4c54ac 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -19,7 +19,7 @@ public static TravelogueResponse getTravelogueResponse() { return TravelogueResponse.builder() .id(1L) .title("제주에 하영 옵서") - .thumbnail("https://url.com/jeju_thumbnail.png") + .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .build(); } @@ -28,7 +28,7 @@ public static Page getTravelogueResponses() { return new PageImpl<>(List.of(TravelogueResponse.builder() .id(1L) .title("제주에 하영 옵서") - .thumbnail("https://url.com/jeju_thumbnail.png") + .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .build())); } @@ -60,6 +60,6 @@ public static TraveloguePositionResponse getTraveloguePositionResponse() { } public static List getTraveloguePhotoUrls() { - return List.of("https://image-url.com/image1.png"); + return List.of("https://dev.touroot.kr/temporary/image1.png"); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index d66ed830..48785c46 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -1,9 +1,11 @@ package kr.touroot.travelogue.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.service.MemberService; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; @@ -14,7 +16,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -28,6 +32,7 @@ TraveloguePlaceService.class, MemberService.class, TravelogueTestHelper.class, + AwsS3Provider.class, }) @ServiceTest class TravelogueFacadeServiceTest { @@ -35,26 +40,33 @@ class TravelogueFacadeServiceTest { private final TravelogueFacadeService service; private final TravelogueTestHelper testHelper; private final DatabaseCleaner databaseCleaner; - - @BeforeEach - void setUp() { - databaseCleaner.executeTruncate(); - } + @MockBean + private final AwsS3Provider s3Provider; @Autowired public TravelogueFacadeServiceTest( TravelogueFacadeService travelogueFacadeService, TravelogueTestHelper travelogueTestHelper, - DatabaseCleaner databaseCleaner + DatabaseCleaner databaseCleaner, + AwsS3Provider s3Provider ) { this.service = travelogueFacadeService; this.testHelper = travelogueTestHelper; this.databaseCleaner = databaseCleaner; + this.s3Provider = s3Provider; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); } @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); + testHelper.initMemberTestData(); MemberAuth memberAuth = new MemberAuth(1L); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java index f5eb8c4d..4cf3b6bc 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -1,9 +1,11 @@ package kr.touroot.travelogue.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import java.util.List; import kr.touroot.global.ServiceTest; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.place.domain.Place; import kr.touroot.travelogue.domain.Travelogue; @@ -15,26 +17,38 @@ import kr.touroot.travelogue.helper.TravelogueTestHelper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @DisplayName("여행기 사진 서비스") -@Import(value = {TraveloguePhotoService.class, TravelogueTestHelper.class}) +@Import(value = {TraveloguePhotoService.class, TravelogueTestHelper.class, AwsS3Provider.class}) @ServiceTest class TraveloguePhotoServiceTest { private final TraveloguePhotoService photoService; private final TravelogueTestHelper testHelper; + @MockBean + private final AwsS3Provider s3Provider; @Autowired - public TraveloguePhotoServiceTest(TraveloguePhotoService photoService, TravelogueTestHelper testHelper) { + public TraveloguePhotoServiceTest( + TraveloguePhotoService photoService, + TravelogueTestHelper testHelper, + AwsS3Provider s3Provider + ) { this.photoService = photoService; this.testHelper = testHelper; + this.s3Provider = s3Provider; } @DisplayName("여행기 사진을 생성한다.") @Test void createPhotos() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn("imageUrl.png"); + List requests = TravelogueRequestFixture.getTraveloguePhotoRequests(); Member author = testHelper.persistMember(); Travelogue travelogue = testHelper.persistTravelogue(author); diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index bff10f5c..644a4a20 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -13,8 +13,10 @@ cloud: aws: s3: bucket: techcourse-project-2024 - image-base-uri: https://dev.touroot.kr/images/ - directory-path: touroot/images/ + image-base-uri: https://dev.touroot.kr/ + base-storage-path: touroot/ + temporary-storage-path: temporary/ + image-storage-path: images/ server: port: 8081 spring: From b954dd22f67cf986a2a6898dac7a2e14d1be11c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:15:47 +0900 Subject: [PATCH 065/108] =?UTF-8?q?[Fix]=20-=20=EC=8D=B8=EB=84=A4=EC=9D=BC?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20S3=20=EC=98=81=EA=B5=AC=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EB=B3=B5=EC=82=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#190)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 썸네일 저장 시 S3 영구 저장소에도 복사하는 기능 구현 * fix: url 파싱 오류 수정 --- .../image/infrastructure/AwsS3Provider.java | 12 +++++---- .../dto/request/TravelogueRequest.java | 4 +-- .../travelogue/service/TravelogueService.java | 5 +++- .../controller/TravelogueControllerTest.java | 6 ++--- .../service/TravelogueFacadeServiceTest.java | 11 +++++--- .../service/TravelogueServiceTest.java | 26 ++++++++++++++----- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java index 7a33a121..79df1ead 100644 --- a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java @@ -84,11 +84,13 @@ private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) } } - public String copyImageToPermanentStorage(String imageKey) { - validateS3Path(imageKey); - String destinationKey = imageKey.replace(temporaryStoragePath, imageStoragePath); - copyFile(imageKey, destinationKey); - return destinationKey; + public String copyImageToPermanentStorage(String imageUrl) { + validateS3Path(imageUrl); + String fileName = imageUrl.substring(imageUrl.lastIndexOf("/") + 1); + String sourceKey = tourootStoragePath + temporaryStoragePath + fileName; + String destinationKey = sourceKey.replace(temporaryStoragePath, imageStoragePath); + copyFile(sourceKey, destinationKey); + return imageUrl.replace(temporaryStoragePath, imageStoragePath); } private void validateS3Path(String imageKey) { diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 16931bad..0da043d3 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -23,7 +23,7 @@ public record TravelogueRequest( List days ) { - public Travelogue toTravelogueOf(Member author) { - return new Travelogue(author, title, thumbnail); + public Travelogue toTravelogueOf(Member author, String url) { + return new Travelogue(author, title, url); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index 4b97d4f2..dc9ca69e 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -1,6 +1,7 @@ package kr.touroot.travelogue.service; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; @@ -15,9 +16,11 @@ public class TravelogueService { private final TravelogueRepository travelogueRepository; + private final AwsS3Provider s3Provider; public Travelogue createTravelogue(Member author, TravelogueRequest request) { - Travelogue travelogue = request.toTravelogueOf(author); + String url = s3Provider.copyImageToPermanentStorage(request.thumbnail()); + Travelogue travelogue = request.toTravelogueOf(author, url); return travelogueRepository.save(travelogue); } diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 14f62cfb..a4e8d084 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -31,14 +31,14 @@ @AcceptanceTest class TravelogueControllerTest { - @LocalServerPort - private int port; private final DatabaseCleaner databaseCleaner; private final TravelogueTestHelper testHelper; private final ObjectMapper objectMapper; private final JwtTokenProvider jwtTokenProvider; @MockBean private final AwsS3Provider s3Provider; + @LocalServerPort + private int port; @Autowired public TravelogueControllerTest( @@ -66,7 +66,7 @@ void setUp() { @Test void createTravelogue() { Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) - .thenReturn("imageUrl.png"); + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); Member member = testHelper.initMemberTestData(); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 48785c46..d3b70dfb 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -1,7 +1,7 @@ package kr.touroot.travelogue.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; @@ -16,7 +16,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @@ -64,8 +63,12 @@ void setUp() { @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { - Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) - .thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); + when(s3Provider.copyImageToPermanentStorage( + TravelogueRequestFixture.getTravelogueRequest().thumbnail()) + ).thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + when(s3Provider.copyImageToPermanentStorage( + TravelogueRequestFixture.getTraveloguePhotoRequests().get(0).url()) + ).thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); testHelper.initMemberTestData(); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 1c573a34..b356ff70 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -4,24 +4,29 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; import kr.touroot.global.ServiceTest; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; +import kr.touroot.travelogue.fixture.TravelogueResponseFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Pageable; @DisplayName("여행기 서비스") -@Import(value = {TravelogueService.class, TravelogueTestHelper.class}) +@Import(value = {TravelogueService.class, TravelogueTestHelper.class, AwsS3Provider.class}) @ServiceTest class TravelogueServiceTest { @@ -30,26 +35,33 @@ class TravelogueServiceTest { private final TravelogueService travelogueService; private final DatabaseCleaner databaseCleaner; private final TravelogueTestHelper testHelper; - - @BeforeEach - void setUp() { - databaseCleaner.executeTruncate(); - } + @MockBean + private final AwsS3Provider s3Provider; @Autowired public TravelogueServiceTest( TravelogueService travelogueService, DatabaseCleaner databaseCleaner, - TravelogueTestHelper testHelper + TravelogueTestHelper testHelper, + AwsS3Provider s3Provider ) { this.travelogueService = travelogueService; this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; + this.s3Provider = s3Provider; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); } @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + Member author = testHelper.persistMember(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); From 7e3522c34fd61a72c89375579f76498cfe1f3fd2 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:30:57 +0900 Subject: [PATCH 066/108] =?UTF-8?q?[Feature]=20-=20=EB=A7=88=EC=9D=B4=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20=EA=B5=AC=ED=98=84=20(#195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 프로필 조회 API 구현 * feat: 내 여행기 조회 API 구현 * feat: 내 여행계획 조회 API 구현 * feat: 내 여행계획/여행기 조회 응답에 id 추가 * test: 여행 계획 기간 조회 테스트 작성 --- .../touroot/global/auth/dto/MemberAuth.java | 2 + .../member/controller/MyPageController.java | 59 +++++++++++++++++++ .../member/dto/MyTravelPlanResponse.java | 19 ++++++ .../member/dto/MyTraveloguesResponse.java | 23 ++++++++ .../touroot/member/dto/ProfileResponse.java | 15 +++++ .../member/service/MyPageFacadeService.java | 48 +++++++++++++++ .../repository/TravelogueRepository.java | 5 ++ .../travelogue/service/TravelogueService.java | 4 ++ .../repository/TravelPlanRepository.java | 5 ++ .../travelplan/service/TravelPlanService.java | 11 ++++ .../controller/TravelPlanControllerTest.java | 2 +- .../helper/TravelPlanTestHelper.java | 6 +- .../service/TravelPlanServiceTest.java | 18 +++++- 13 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/member/controller/MyPageController.java create mode 100644 backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java create mode 100644 backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java create mode 100644 backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java create mode 100644 backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java diff --git a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java index d2dce76c..ab6c2c36 100644 --- a/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java +++ b/backend/src/main/java/kr/touroot/global/auth/dto/MemberAuth.java @@ -1,6 +1,8 @@ package kr.touroot.global.auth.dto; +import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.constraints.NotNull; +@Hidden public record MemberAuth(@NotNull Long memberId) { } diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java new file mode 100644 index 00000000..9fca1559 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -0,0 +1,59 @@ +package kr.touroot.member.controller; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.member.dto.MyTravelPlanResponse; +import kr.touroot.member.dto.MyTraveloguesResponse; +import kr.touroot.member.dto.ProfileResponse; +import kr.touroot.member.service.MyPageFacadeService; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.converters.models.PageableAsQueryParam; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "마이 페이지") +@RequiredArgsConstructor +@RestController +@RequestMapping("api/v1/member/me") +public class MyPageController { + + private final MyPageFacadeService myPageFacadeService; + + @GetMapping("/profile") + public ResponseEntity readProfile(@NotNull MemberAuth memberAuth) { + ProfileResponse data = myPageFacadeService.readProfile(memberAuth); + return ResponseEntity.ok(data); + } + + @PageableAsQueryParam + @GetMapping("/travelogues") + public ResponseEntity> readTravelogues( + @NotNull MemberAuth memberAuth, + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + Page data = myPageFacadeService.readTravelogues(memberAuth, pageable); + return ResponseEntity.ok(data); + } + + @PageableAsQueryParam + @GetMapping("/travel-plans") + public ResponseEntity> readTravelPlans( + @NotNull MemberAuth memberAuth, + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) + Pageable pageable + ) { + Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); + return ResponseEntity.ok(data); + } +} diff --git a/backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java b/backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java new file mode 100644 index 00000000..52b55599 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/MyTravelPlanResponse.java @@ -0,0 +1,19 @@ +package kr.touroot.member.dto; + +import kr.touroot.travelplan.domain.TravelPlan; +import lombok.Builder; + +import java.time.LocalDate; + +@Builder +public record MyTravelPlanResponse(long id, String title, LocalDate startDate, LocalDate endDate) { + + public static MyTravelPlanResponse of(TravelPlan travelPlan, int period) { + return MyTravelPlanResponse.builder() + .id(travelPlan.getId()) + .title(travelPlan.getTitle()) + .startDate(travelPlan.getStartDate()) + .endDate(travelPlan.getStartDate().plusDays(period)) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java b/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java new file mode 100644 index 00000000..7966c8fa --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java @@ -0,0 +1,23 @@ +package kr.touroot.member.dto; + +import kr.touroot.travelogue.domain.Travelogue; +import lombok.Builder; + +import java.time.format.DateTimeFormatter; + +@Builder +public record MyTraveloguesResponse(long id, String title, String thumbnailUrl, String createdAt) { + + public static MyTraveloguesResponse from(Travelogue travelogue) { + String createdAt = travelogue.getCreatedAt() + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + return MyTraveloguesResponse.builder() + .id(travelogue.getId()) + .title(travelogue.getTitle()) + .createdAt(createdAt) + .thumbnailUrl(travelogue.getThumbnail()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java b/backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java new file mode 100644 index 00000000..3a961ef5 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/ProfileResponse.java @@ -0,0 +1,15 @@ +package kr.touroot.member.dto; + +import kr.touroot.member.domain.Member; +import lombok.Builder; + +@Builder +public record ProfileResponse(String profileImageUrl, String nickname) { + + public static ProfileResponse from(Member member) { + return ProfileResponse.builder() + .profileImageUrl(member.getProfileImageUrl()) + .nickname(member.getNickname()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java new file mode 100644 index 00000000..a8fad42f --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -0,0 +1,48 @@ +package kr.touroot.member.service; + +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.member.domain.Member; +import kr.touroot.member.dto.MyTravelPlanResponse; +import kr.touroot.member.dto.MyTraveloguesResponse; +import kr.touroot.member.dto.ProfileResponse; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.service.TravelogueService; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.service.TravelPlanService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class MyPageFacadeService { + + private final MemberService memberService; + private final TravelogueService travelogueService; + private final TravelPlanService travelPlanService; + + public ProfileResponse readProfile(MemberAuth memberAuth) { + Member member = memberService.getById(memberAuth.memberId()); + return ProfileResponse.from(member); + } + + public Page readTravelogues(MemberAuth memberAuth, Pageable pageable) { + Member member = memberService.getById(memberAuth.memberId()); + Page travelogues = travelogueService.findAllByMember(member, pageable); + + return travelogues.map(MyTraveloguesResponse::from); + } + + public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { + Member member = memberService.getById(memberAuth.memberId()); + Page travelPlans = travelPlanService.getAllByAuthor(member, pageable); + + return travelPlans.map(this::getMyTravelPlanResponse); + } + + private MyTravelPlanResponse getMyTravelPlanResponse(TravelPlan travelPlan) { + int period = travelPlanService.calculateTravelPeriod(travelPlan); + return MyTravelPlanResponse.of(travelPlan, period); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java index 5e17ae01..60609f7b 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueRepository.java @@ -1,7 +1,12 @@ package kr.touroot.travelogue.repository; +import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface TravelogueRepository extends JpaRepository { + + Page findAllByAuthor(Member author, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index dc9ca69e..352a7004 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -32,4 +32,8 @@ public Travelogue getTravelogueById(Long id) { public Page findAll(Pageable pageable) { return travelogueRepository.findAll(pageable); } + + public Page findAllByMember(Member member, Pageable pageable) { + return travelogueRepository.findAllByAuthor(member, pageable); + } } diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java index 0489b9fa..b0786bcf 100644 --- a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanRepository.java @@ -1,7 +1,12 @@ package kr.touroot.travelplan.repository; +import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface TravelPlanRepository extends JpaRepository { + + Page findAllByAuthor(Member member, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index bc1b79eb..b4d16ddb 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -21,6 +21,8 @@ import kr.touroot.travelplan.repository.TravelPlanPlaceRepository; import kr.touroot.travelplan.repository.TravelPlanRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -121,4 +123,13 @@ private List getTravelPlanPlaceResponses(TravelPlanDay .map(TravelPlanPlaceResponse::from) .toList(); } + + public Page getAllByAuthor(Member member, Pageable pageable) { + return travelPlanRepository.findAllByAuthor(member, pageable); + } + + public int calculateTravelPeriod(TravelPlan travelPlan) { + return travelPlanDayRepository.findByPlan(travelPlan) + .size(); + } } diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index 561de6ef..e8c06393 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -152,7 +152,7 @@ void readTravelPlanWithNonExist() { @Test void readTravelPlanWithNotAuthor() { // given - long id = testHelper.initTravelPlanTestData(member); + long id = testHelper.initTravelPlanTestData(member).getId(); Member notAuthor = testHelper.initMemberTestData(); String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 3e8e7726..69d6e445 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -59,7 +59,7 @@ public static TravelPlanPlace getTravelPlanPlace(String description, int order, return new TravelPlanPlace(description, order, day, place); } - public long initTravelPlanTestData(Member author) { + public TravelPlan initTravelPlanTestData(Member author) { TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); Place place = getPlace("장소", "37.5175896", "127.0867236", ""); @@ -68,7 +68,9 @@ public long initTravelPlanTestData(Member author) { travelPlanRepository.save(travelPlan); travelPlanDayRepository.save(travelPlanDay); placeRepository.save(place); - return travelPlanPlaceRepository.save(travelPlanPlace).getId(); + travelPlanPlaceRepository.save(travelPlanPlace); + + return travelPlan; } public Member initMemberTestData() { diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index a9c8a1c1..6ec7bb19 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -5,6 +5,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; @@ -107,7 +108,7 @@ void createTravelPlanWithInvalidStartDate() { @Test void readTravelPlan() { // given - Long id = testHelper.initTravelPlanTestData(author); + Long id = testHelper.initTravelPlanTestData(author).getId(); // when TravelPlanResponse actual = travelPlanService.readTravelPlan(id, memberAuth); @@ -133,7 +134,7 @@ void readTravelPlanWitNonExist() { @Test void readTravelPlanWithNotAuthor() { // given - Long id = testHelper.initTravelPlanTestData(author); + Long id = testHelper.initTravelPlanTestData(author).getId(); MemberAuth notAuthor = new MemberAuth(testHelper.initMemberTestData().getId()); // when & then @@ -141,4 +142,17 @@ void readTravelPlanWithNotAuthor() { .isInstanceOf(ForbiddenException.class) .hasMessage("여행 계획은 작성자만 조회할 수 있습니다."); } + + @DisplayName("여행 계획 서비스는 여행 계획 일자를 계산해 반환한다.") + @Test + void calculateTravelPeriod() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); + + // when + int actual = travelPlanService.calculateTravelPeriod(travelPlan); + + // then + assertThat(actual).isEqualTo(1); + } } From 58e7457e1f643af549a5a7690aa69bbbca0f4433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Sun, 4 Aug 2024 15:50:24 +0900 Subject: [PATCH 067/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EA=B3=B5=EC=9C=A0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: TravelPlan 공유를 위한 URL 생성에 사용되는 Key(UUID) 필드 추가 * refactor: TravelPlan 생성 과정과 응답 DTO에 UUID 필드 추가 * refactor: TravelPlan UUID 필드 추가에 따른 테스트 코드 수정 * feat: 공유된 여행 계획 조회 기능 구현 * feat: Swagger DTO 필드 example 작성 * fix: 공유된 여행 계획 조회 엔드포인트 수정 * test: 여행 계획 공유 테스트 작성 * refactor: 여행 계획 공유 키 생성 로직 DTO에서 서비스로 이동 개선 * feat: 로그인 되지 않은 유저도 공유된 여행 계획을 조회할 수 있도록 기능 추가 --------- Co-authored-by: hangillee --- .../kr/touroot/global/auth/JwtAuthFilter.java | 6 +- .../controller/TravelPlanController.java | 31 ++++++- .../touroot/travelplan/domain/TravelPlan.java | 12 ++- .../dto/request/TravelPlanCreateRequest.java | 6 +- .../response/TravelPlanCreateResponse.java | 2 +- .../dto/response/TravelPlanPlaceResponse.java | 6 +- .../response/TravelPlanPositionResponse.java | 4 +- .../dto/response/TravelPlanResponse.java | 11 ++- .../repository/TravelPlanRepository.java | 4 + .../travelplan/service/TravelPlanService.java | 20 ++++- .../controller/TravelPlanControllerTest.java | 89 +++++++++++++++++-- .../travelplan/domain/TravelPlanTest.java | 16 ++-- .../travelplan/fixture/TravelPlanFixture.java | 3 +- .../helper/TravelPlanTestHelper.java | 3 +- .../service/TravelPlanServiceTest.java | 37 ++++++-- 15 files changed, 202 insertions(+), 48 deletions(-) diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index e3ff3c14..9d2b7c03 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -5,6 +5,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.auth.dto.HttpRequestInfo; import kr.touroot.global.exception.dto.ExceptionResponse; @@ -17,9 +19,6 @@ import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; -import java.util.List; - @RequiredArgsConstructor @Slf4j @Component @@ -39,6 +38,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/login/**"), + new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/shared/**"), new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); diff --git a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java index e0d9dfeb..2810b101 100644 --- a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java @@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.net.URI; +import java.util.UUID; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; @@ -16,9 +18,12 @@ import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.net.URI; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @Tag(name = "여행 계획") @RequiredArgsConstructor @@ -80,4 +85,24 @@ public ResponseEntity readTravelPlan( TravelPlanResponse data = travelPlanService.readTravelPlan(id, memberAuth); return ResponseEntity.ok(data); } + + @Operation(summary = "공유된 여행 계획 상세 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "여행 계획 상세 조회가 정상적으로 성공했을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않은 여행 계획을 조회할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @GetMapping("shared/{shareKey}") + public ResponseEntity readSharedTravelPlan( + @Parameter(description = "여행 계획 공유 키") @PathVariable UUID shareKey + ) { + TravelPlanResponse data = travelPlanService.readTravelPlan(shareKey); + return ResponseEntity.ok(data); + } } diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 5d56ed08..22f93551 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -9,6 +9,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import java.time.LocalDate; +import java.util.UUID; import kr.touroot.global.entity.BaseEntity; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; @@ -23,6 +24,7 @@ public class TravelPlan extends BaseEntity { private static final int TITLE_MIN_LENGTH = 1; private static final int TITLE_MAX_LENGTH = 20; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -33,20 +35,24 @@ public class TravelPlan extends BaseEntity { @Column(nullable = false) private LocalDate startDate; + @Column(nullable = false) + private UUID shareKey; + @JoinColumn(name = "author_id", nullable = false) @ManyToOne(fetch = FetchType.LAZY) private Member author; - public TravelPlan(Long id, String title, LocalDate startDate, Member author) { + public TravelPlan(Long id, String title, LocalDate startDate, UUID shareKey, Member author) { validate(title, startDate, author); this.id = id; this.title = title; this.startDate = startDate; + this.shareKey = shareKey; this.author = author; } - public TravelPlan(String title, LocalDate startDate, Member author) { - this(null, title, startDate, author); + public TravelPlan(String title, LocalDate startDate, UUID shareKey, Member author) { + this(null, title, startDate, shareKey, author); } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java index 6e10304a..ce493c83 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java @@ -7,7 +7,7 @@ import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.util.List; - +import java.util.UUID; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; import lombok.Builder; @@ -27,7 +27,7 @@ public record TravelPlanCreateRequest( List days ) { - public TravelPlan toTravelPlan(Member author) { - return new TravelPlan(title, startDate, author); + public TravelPlan toTravelPlan(Member author, UUID shareKey) { + return new TravelPlan(title, startDate, shareKey, author); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java index 0962f2fe..fbfaada1 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; public record TravelPlanCreateResponse( - @Schema(description = "생성된 여행 계획 id") + @Schema(description = "생성된 여행 계획 id", example = "1") Long id ) { } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java index c5d2f540..27c7bf1b 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java @@ -1,15 +1,15 @@ package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; import kr.touroot.place.domain.Place; import kr.touroot.travelplan.domain.TravelPlanPlace; +import lombok.Builder; @Builder public record TravelPlanPlaceResponse( - @Schema(description = "여행 장소 이름") String placeName, + @Schema(description = "여행 장소 이름", example = "잠실한강공원") String placeName, @Schema(description = "여행 장소 위치") TravelPlanPositionResponse position, - @Schema(description = "여행 장소 설명") String description + @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") String description ) { public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) { diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java index 349d557e..75bc788e 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java @@ -6,8 +6,8 @@ @Builder public record TravelPlanPositionResponse( - @Schema(description = "여행 장소 위도") String lat, - @Schema(description = "여행 계획 경도") String lng + @Schema(description = "여행 장소 위도", example = "37.5175896") String lat, + @Schema(description = "여행 계획 경도", example = "127.0867236") String lng ) { public static TravelPlanPositionResponse from(Place place) { diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java index fac07330..c9cb3e2b 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java @@ -3,15 +3,17 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; import java.util.List; +import java.util.UUID; import kr.touroot.travelplan.domain.TravelPlan; import lombok.Builder; @Builder public record TravelPlanResponse( - @Schema(description = "여행 계획 id") Long id, - @Schema(description = "여행 계획 제목") String title, - @Schema(description = "여행 시작일") LocalDate startDate, - @Schema(description = "여행 계획 날짜별 정보") List days + @Schema(description = "여행 계획 id", example = "1") Long id, + @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") String title, + @Schema(description = "여행 시작일", example = "2024-11-16") LocalDate startDate, + @Schema(description = "여행 계획 날짜별 정보") List days, + @Schema(description = "여행 계획 공유 share Key") UUID shareKey ) { public static TravelPlanResponse of(TravelPlan travelPlan, List days) { @@ -20,6 +22,7 @@ public static TravelPlanResponse of(TravelPlan travelPlan, List { Page findAllByAuthor(Member member, Pageable pageable); + + Optional findByShareKey(UUID shareKey); } diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index b4d16ddb..bc9e349c 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -1,5 +1,8 @@ package kr.touroot.travelplan.service; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; @@ -26,9 +29,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Comparator; -import java.util.List; - @RequiredArgsConstructor @Service public class TravelPlanService { @@ -42,7 +42,7 @@ public class TravelPlanService { @Transactional public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request, MemberAuth memberAuth) { Member author = getMemberByMemberAuth(memberAuth); - TravelPlan travelPlan = request.toTravelPlan(author); + TravelPlan travelPlan = request.toTravelPlan(author, UUID.randomUUID()); validStartDate(travelPlan); TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); @@ -95,6 +95,13 @@ public TravelPlanResponse readTravelPlan(Long planId, MemberAuth memberAuth) { return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } + @Transactional(readOnly = true) + public TravelPlanResponse readTravelPlan(UUID shareKey) { + TravelPlan travelPlan = getTravelPlanByShareKey(shareKey); + + return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + } + private void validateAuthor(TravelPlan travelPlan, Member member) { if (!travelPlan.isAuthor(member)) { throw new ForbiddenException("여행 계획은 작성자만 조회할 수 있습니다."); @@ -106,6 +113,11 @@ private TravelPlan getTravelPlanById(Long planId) { .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); } + private TravelPlan getTravelPlanByShareKey(UUID shareKey) { + return travelPlanRepository.findByShareKey(shareKey) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); + } + private List getTravelPlanDayResponses(TravelPlan travelPlan) { List planDays = travelPlanDayRepository.findByPlan(travelPlan); diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index e8c06393..c570499e 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -1,10 +1,17 @@ package kr.touroot.travelplan.controller; +import static org.hamcrest.Matchers.is; + +import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; @@ -18,17 +25,13 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; -import java.time.LocalDate; -import java.util.List; - -import static org.hamcrest.Matchers.is; - @DisplayName("여행 계획 컨트롤러") @AcceptanceTest class TravelPlanControllerTest { @LocalServerPort private int port; + private final ObjectMapper objectMapper; private final DatabaseCleaner databaseCleaner; private final JwtTokenProvider jwtTokenProvider; private final TravelPlanTestHelper testHelper; @@ -39,8 +42,10 @@ class TravelPlanControllerTest { public TravelPlanControllerTest( DatabaseCleaner databaseCleaner, TravelPlanTestHelper testHelper, - JwtTokenProvider jwtTokenProvider + JwtTokenProvider jwtTokenProvider, + ObjectMapper objectMapper ) { + this.objectMapper = objectMapper; this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; this.jwtTokenProvider = jwtTokenProvider; @@ -165,7 +170,77 @@ void readTravelPlanWithNotAuthor() { .then().log().all() .statusCode(403) .body("message", is("여행 계획은 작성자만 조회할 수 있습니다.")); + } + + @DisplayName("여행 계획 공유 키를 통해 여행 계획을 조회할 수 있다") + @Test + void readTravelPlanByShareKey() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .get("/api/v1/travel-plans/shared/" + travelPlan.getShareKey()) + .then().log().all() + .statusCode(200) + .body("shareKey", is(travelPlan.getShareKey().toString())); + } - // then + @DisplayName("공유된 여행 계획은 작성자가 아닌 회원도 조회할 수 있다") + @Test + void readTravelPlanByShareKeyFromNoAuthor() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); + Member notAuthor = testHelper.initMemberTestData(); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + notAuthorAccessToken) + .when().log().all() + .get("/api/v1/travel-plans/shared/" + travelPlan.getShareKey()) + .then().log().all() + .statusCode(200) + .body("shareKey", is(travelPlan.getShareKey().toString())); + } + + @DisplayName("공유된 여행 계획은 로그인되지 않은 유저도 조회할 수 있다") + @Test + void readTravelPlanByNotLoginUser() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); + Member notAuthor = testHelper.initMemberTestData(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().log().all() + .get("/api/v1/travel-plans/shared/" + travelPlan.getShareKey()) + .then().log().all() + .statusCode(200) + .body("shareKey", is(travelPlan.getShareKey().toString())); + } + + @DisplayName("공유된 여행 계획을 조회할 때 존재하지 않는 공유 키로 조회할 경우 400을 응답한다") + @Test + void readTravelPlanByInvalidShareKey() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .get("/api/v1/travel-plans/shared/" + UUID.randomUUID()) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행 계획입니다.")); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index 407af5b9..f47a538f 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; +import java.util.UUID; import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; @@ -18,19 +19,20 @@ class TravelPlanTest { private static final Member VALID_AUTHOR = MemberFixture.MEMBER_1; private static final String VALID_TITLE = "제주도 여행 계획"; + private static final UUID VALID_UUID = UUID.randomUUID(); private static final LocalDate VALID_START_DATE = LocalDate.now().plusDays(2); @DisplayName("올바른 여행 계획 생성 시에는 예외가 발생하지 않는다") @Test void createTravelPlanWithValidData() { - assertThatCode(() -> new TravelPlan(VALID_TITLE, VALID_START_DATE, VALID_AUTHOR)) + assertThatCode(() -> new TravelPlan(VALID_TITLE, VALID_START_DATE, VALID_UUID, VALID_AUTHOR)) .doesNotThrowAnyException(); } @DisplayName("여행 계획의 제목이 비어 있는 경우 여행 계획 생성 시 예외가 발생한다") @Test void createTravelPlanWithNullTitle() { - assertThatThrownBy(() -> new TravelPlan(null, VALID_START_DATE, VALID_AUTHOR)) + assertThatThrownBy(() -> new TravelPlan(null, VALID_START_DATE, VALID_UUID, VALID_AUTHOR)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); } @@ -38,7 +40,7 @@ void createTravelPlanWithNullTitle() { @DisplayName("여행 계획의 시작 날짜가 비어 있는 경우 여행 계획 생성 시 예외가 발생한다") @Test void createTravelPlanWithNullStartDate() { - assertThatThrownBy(() -> new TravelPlan(VALID_TITLE, null, VALID_AUTHOR)) + assertThatThrownBy(() -> new TravelPlan(VALID_TITLE, null, VALID_UUID, VALID_AUTHOR)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); } @@ -47,7 +49,7 @@ void createTravelPlanWithNullStartDate() { @ParameterizedTest @ValueSource(strings = {" ", " ", " "}) void createTravelPlanWithBlankTitle(String blank) { - assertThatThrownBy(() -> new TravelPlan(blank, VALID_START_DATE, VALID_AUTHOR)) + assertThatThrownBy(() -> new TravelPlan(blank, VALID_START_DATE, VALID_UUID, VALID_AUTHOR)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획에서 제목은 공백 문자로만 이루어질 수 없습니다"); } @@ -57,7 +59,7 @@ void createTravelPlanWithBlankTitle(String blank) { void createTravelPlanWithInvalidLengthTitle() { String length21 = "서울 명동: 패션 쇼핑과 길거리 음식,"; - assertThatThrownBy(() -> new TravelPlan(length21, VALID_START_DATE, VALID_AUTHOR)) + assertThatThrownBy(() -> new TravelPlan(length21, VALID_START_DATE, VALID_UUID, VALID_AUTHOR)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획은 1자 이상, 20자 이하여야 합니다"); } @@ -65,7 +67,7 @@ void createTravelPlanWithInvalidLengthTitle() { @DisplayName("여행 계획은 지난 날짜를 검증할 시 예외가 발생한다.") @Test void validateStartDate() { - TravelPlan travelPlan = new TravelPlan(VALID_TITLE, LocalDate.MIN, VALID_AUTHOR); + TravelPlan travelPlan = new TravelPlan(VALID_TITLE, LocalDate.MIN, VALID_UUID, VALID_AUTHOR); assertThatCode(travelPlan::validateStartDate) .isInstanceOf(BadRequestException.class) @@ -82,7 +84,7 @@ void validateStartDate() { void validateAuthor() { // given Member author = new Member(1L, 1L, "tester", "http://url.com"); - TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN, author); + TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN, VALID_UUID, author); Member notAuthor = new Member(2L, 2L, "tester2", "http://url.com"); // when diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java index 1ee228cc..770c36e4 100644 --- a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java @@ -1,6 +1,7 @@ package kr.touroot.travelplan.fixture; import java.time.LocalDate; +import java.util.UUID; import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; @@ -20,6 +21,6 @@ public enum TravelPlanFixture { } public TravelPlan get() { - return new TravelPlan(title, startDate, author); + return new TravelPlan(title, startDate, UUID.randomUUID(), author); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 69d6e445..91be7bca 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -1,5 +1,6 @@ package kr.touroot.travelplan.helper; +import java.util.UUID; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; @@ -48,7 +49,7 @@ public static Place getPlace(String name, String latitude, String longitude, Str } public static TravelPlan getTravelPlan(String title, LocalDate startDate, Member author) { - return new TravelPlan(title, startDate, author); + return new TravelPlan(title, startDate, UUID.randomUUID(), author); } public static TravelPlanDay getTravelPlanDay(int order, TravelPlan travelPlan) { diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index 6ec7bb19..d277e113 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -1,5 +1,11 @@ package kr.touroot.travelplan.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; @@ -20,12 +26,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; -import java.time.LocalDate; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - @DisplayName("여행 계획 서비스") @Import(value = {TravelPlanService.class, TravelPlanTestHelper.class}) @ServiceTest @@ -155,4 +155,29 @@ void calculateTravelPeriod() { // then assertThat(actual).isEqualTo(1); } + + @DisplayName("여행 계획 서비스는 공유 키로 여행 계획을 조회할 수 있다") + @Test + void readTravelPlanByShareKey() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); + + // when + TravelPlanResponse actual = travelPlanService.readTravelPlan(travelPlan.getShareKey()); + + // then + assertThat(actual.shareKey()).isEqualTo(travelPlan.getShareKey()); + } + + @DisplayName("여행 계획 서비스는 존재하지 않는 공유 키로 여행 계획을 조회할 경우 예외가 발생한다") + @Test + void readTravelPlanByInvalidShareKey() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); + + // when & then + assertThatThrownBy(() -> travelPlanService.readTravelPlan(UUID.randomUUID())) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행 계획입니다."); + } } From 099b3b62cf29855757795f194d94cb14eabef8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Sun, 4 Aug 2024 21:04:24 +0900 Subject: [PATCH 068/108] =?UTF-8?q?[Fix]=20-=20=EC=97=AC=ED=96=89=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=EC=9D=B4=20=EC=A0=80=EC=9E=A5=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0UUI?= =?UTF-8?q?D=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/kr/touroot/travelplan/domain/TravelPlan.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 22f93551..973aa7eb 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -35,7 +35,7 @@ public class TravelPlan extends BaseEntity { @Column(nullable = false) private LocalDate startDate; - @Column(nullable = false) + @Column(columnDefinition = "CHAR(36)", nullable = false) private UUID shareKey; @JoinColumn(name = "author_id", nullable = false) From c425adea16eae0abec98680660bb60da9707899f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Sun, 4 Aug 2024 21:54:56 +0900 Subject: [PATCH 069/108] =?UTF-8?q?[Fix]=20-=20DB=20=EC=BB=A4=EB=84=A5?= =?UTF-8?q?=EC=85=98=20=EC=9D=B8=EC=BD=94=EB=94=A9=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=A7=80=EC=A0=95=20(#213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: UUID 컬럼 정의 명시적 작성 * fix: DB 커넥션 인코딩 파라미터 추가 --- backend/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 85ce7c2f..5fb9c7ad 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -69,7 +69,7 @@ spring: enabled: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ENC(FgbVXdH4a5/EkMxdmYfPhFKAOwn1w+/CnmWDcN9p6fOVP6mP9coMEYezCPCNf95h) + url: ENC(j092CA8+WPUDVjhMQtLrBk+DyClgx5Iyc54arocVqdj4MLBu+YDJrclOKlqvC0z4BuI5gcjCpkEDZFhsJWBHRt0upwb0D45E3NZSvjQzPuA1XnG7dOSe4gXaYo+PDk/e) username: ENC(SJznQPqjlZuw3qf8kv9IJQ==) password: ENC(HsOo6wWp//egPPsSG6Wf40eF1Q2sVKfGuH4zGTL81Mw=) jpa: From 4cc6d49044475ef14ade2e4d1d9865f3ea300a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Sun, 4 Aug 2024 22:08:50 +0900 Subject: [PATCH 070/108] =?UTF-8?q?[Fix]=20-=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20url=20=EC=9B=90=EB=B3=B5=20(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: UUID 컬럼 정의 명시적 작성 * fix: DB 커넥션 인코딩 파라미터 추가 * fix: database url revert * fix: DB 주소 원복 --- backend/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5fb9c7ad..85ce7c2f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -69,7 +69,7 @@ spring: enabled: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ENC(j092CA8+WPUDVjhMQtLrBk+DyClgx5Iyc54arocVqdj4MLBu+YDJrclOKlqvC0z4BuI5gcjCpkEDZFhsJWBHRt0upwb0D45E3NZSvjQzPuA1XnG7dOSe4gXaYo+PDk/e) + url: ENC(FgbVXdH4a5/EkMxdmYfPhFKAOwn1w+/CnmWDcN9p6fOVP6mP9coMEYezCPCNf95h) username: ENC(SJznQPqjlZuw3qf8kv9IJQ==) password: ENC(HsOo6wWp//egPPsSG6Wf40eF1Q2sVKfGuH4zGTL81Mw=) jpa: From fc9284e20fc6ff52304e69444b784617a4b023dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Mon, 5 Aug 2024 22:34:56 +0900 Subject: [PATCH 071/108] =?UTF-8?q?[Fix]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20UUID=20=ED=95=98=EC=9D=B4=EB=B2=84?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=ED=8A=B8=20=EB=A7=A4=ED=95=91=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EA=B3=BC=20=EA=B0=99=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 컬럼 정의 override 제거 * test: 공유 키 null 검증 추가 --- .../kr/touroot/travelplan/domain/TravelPlan.java | 15 +++++++-------- .../touroot/travelplan/domain/TravelPlanTest.java | 13 +++++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 973aa7eb..03e6091a 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -35,7 +35,7 @@ public class TravelPlan extends BaseEntity { @Column(nullable = false) private LocalDate startDate; - @Column(columnDefinition = "CHAR(36)", nullable = false) + @Column(nullable = false) private UUID shareKey; @JoinColumn(name = "author_id", nullable = false) @@ -43,7 +43,7 @@ public class TravelPlan extends BaseEntity { private Member author; public TravelPlan(Long id, String title, LocalDate startDate, UUID shareKey, Member author) { - validate(title, startDate, author); + validate(title, startDate, author, shareKey); this.id = id; this.title = title; this.startDate = startDate; @@ -55,16 +55,15 @@ public TravelPlan(String title, LocalDate startDate, UUID shareKey, Member autho this(null, title, startDate, shareKey, author); } - - private void validate(String title, LocalDate startDate, Member author) { - validateNotNull(title, startDate, author); + private void validate(String title, LocalDate startDate, Member author, UUID shareKey) { + validateNotNull(title, startDate, author, shareKey); validateNotBlank(title); validateTitleLength(title); } - private void validateNotNull(String title, LocalDate startDate, Member author) { - if (title == null || startDate == null || author == null) { - throw new BadRequestException("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); + private void validateNotNull(String title, LocalDate startDate, Member author, UUID shareKey) { + if (title == null || startDate == null || author == null || shareKey == null) { + throw new BadRequestException("여행 계획에서 제목과 시작 날짜, 공유 키, 그리고 작성자는 비어 있을 수 없습니다"); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index f47a538f..e3d1c609 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -21,6 +21,7 @@ class TravelPlanTest { private static final String VALID_TITLE = "제주도 여행 계획"; private static final UUID VALID_UUID = UUID.randomUUID(); private static final LocalDate VALID_START_DATE = LocalDate.now().plusDays(2); + public static final String EMPTY_FIELD_EXIST_MESSAGE = "여행 계획에서 제목과 시작 날짜, 공유 키, 그리고 작성자는 비어 있을 수 없습니다"; @DisplayName("올바른 여행 계획 생성 시에는 예외가 발생하지 않는다") @Test @@ -34,7 +35,7 @@ void createTravelPlanWithValidData() { void createTravelPlanWithNullTitle() { assertThatThrownBy(() -> new TravelPlan(null, VALID_START_DATE, VALID_UUID, VALID_AUTHOR)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); + .hasMessage(EMPTY_FIELD_EXIST_MESSAGE); } @DisplayName("여행 계획의 시작 날짜가 비어 있는 경우 여행 계획 생성 시 예외가 발생한다") @@ -42,7 +43,15 @@ void createTravelPlanWithNullTitle() { void createTravelPlanWithNullStartDate() { assertThatThrownBy(() -> new TravelPlan(VALID_TITLE, null, VALID_UUID, VALID_AUTHOR)) .isInstanceOf(BadRequestException.class) - .hasMessage("여행 계획에서 제목과 시작 날짜, 그리고 작성자는 비어 있을 수 없습니다"); + .hasMessage(EMPTY_FIELD_EXIST_MESSAGE); + } + + @DisplayName("여행 게획의 공유 키가 비어 있는 경우 여행 계획 생성 시 예외가 발생한다") + @Test + void createTravelPlanWithNullShareKey() { + assertThatThrownBy(() -> new TravelPlan(VALID_TITLE, null, VALID_UUID, VALID_AUTHOR)) + .isInstanceOf(BadRequestException.class) + .hasMessage(EMPTY_FIELD_EXIST_MESSAGE); } @DisplayName("여행 계획 제목이 공백 문자로만 이루어져 있는 경우 생성 시 예외가 발생한다") From bd8f6d3e6b52e855864f1eeebab259c82f080d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Mon, 5 Aug 2024 22:37:07 +0900 Subject: [PATCH 072/108] =?UTF-8?q?[Feature]=20-=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=85=EC=84=B8=20=EC=88=98=EC=A0=95=20(#202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 로그인 API 명세 수정 - 쿼리파라미터로 인코딩된 redirectUri를 받도록 수정 * fix: 로그인 명세 POST 메서드로 수정 및 로컬 redirect default 제거 * test: 로그인 명세 수정에 따른 테스트 코드 수정 --- .../authentication/controller/LoginController.java | 11 +++++++---- .../infrastructure/KakaoOauthClient.java | 11 ++++------- .../infrastructure/KakaoOauthProvider.java | 6 +++--- .../touroot/authentication/service/LoginService.java | 7 +++++-- .../java/kr/touroot/global/auth/JwtAuthFilter.java | 2 +- backend/src/main/resources/application.yml | 4 ---- .../controller/LoginControllerTest.java | 9 +++++---- .../authentication/service/LoginServiceTest.java | 9 +++++---- 8 files changed, 30 insertions(+), 29 deletions(-) diff --git a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java index 517b8ece..066785f2 100644 --- a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java @@ -11,7 +11,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -36,9 +36,12 @@ public class LoginController { content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ) }) - @GetMapping("/oauth/kakao") - public ResponseEntity login(@RequestParam(name = "code") String authorizationCode) { + @PostMapping("/oauth/kakao") + public ResponseEntity login( + @RequestParam(name = "code") String authorizationCode, + @RequestParam(name = "redirectUri") String encodedRedirectUri + ) { return ResponseEntity.ok() - .body(loginService.login(authorizationCode)); + .body(loginService.login(authorizationCode, encodedRedirectUri)); } } diff --git a/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java index 7f424f59..b3eb1459 100644 --- a/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java @@ -26,19 +26,16 @@ public class KakaoOauthClient { private final String userInformationRequestUri; private final String accessTokenRequestUri; private final String restApiKey; - private final String redirectUri; private final RestClient restClient; public KakaoOauthClient( @Value("${oauth.kakao.user-information-request-uri}") String userInformationRequestUri, @Value("${oauth.kakao.access-token-request-uri}") String accessTokenRequestUri, - @Value("${oauth.kakao.rest-api-key}") String restApiKey, - @Value("${oauth.kakao.redirect-uri}") String redirectUri + @Value("${oauth.kakao.rest-api-key}") String restApiKey ) { this.userInformationRequestUri = userInformationRequestUri; this.accessTokenRequestUri = accessTokenRequestUri; this.restApiKey = restApiKey; - this.redirectUri = redirectUri; this.restClient = buildRestClient(); } @@ -54,8 +51,8 @@ private RestClient buildRestClient() { .build(); } - public OauthUserInformationResponse requestUserInformation(String authorizationCode) { - KakaoAccessTokenResponse kakaoAccessTokenResponse = requestAccessToken(authorizationCode); + public OauthUserInformationResponse requestUserInformation(String authorizationCode, String redirectUri) { + KakaoAccessTokenResponse kakaoAccessTokenResponse = requestAccessToken(authorizationCode, redirectUri); return restClient.get() .uri(userInformationRequestUri) @@ -66,7 +63,7 @@ public OauthUserInformationResponse requestUserInformation(String authorizationC .getBody(); } - private KakaoAccessTokenResponse requestAccessToken(String authorizationCode) { + private KakaoAccessTokenResponse requestAccessToken(String authorizationCode, String redirectUri) { MultiValueMap params = new LinkedMultiValueMap<>(); params.add("code", authorizationCode); params.add("client_id", restApiKey); diff --git a/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java index 793212c8..a933a3cf 100644 --- a/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthProvider.java @@ -1,8 +1,8 @@ package kr.touroot.authentication.infrastructure; +import kr.touroot.authentication.dto.response.OauthUserInformationResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import kr.touroot.authentication.dto.response.OauthUserInformationResponse; @RequiredArgsConstructor @Component @@ -10,7 +10,7 @@ public class KakaoOauthProvider { private final KakaoOauthClient kakaoOauthClient; - public OauthUserInformationResponse getUserInformation(String authorizationCode) { - return kakaoOauthClient.requestUserInformation(authorizationCode); + public OauthUserInformationResponse getUserInformation(String authorizationCode, String redirectUri) { + return kakaoOauthClient.requestUserInformation(authorizationCode, redirectUri); } } diff --git a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java index 48804db3..0999c89c 100644 --- a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java @@ -1,5 +1,7 @@ package kr.touroot.authentication.service; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.dto.response.OauthUserInformationResponse; import kr.touroot.authentication.infrastructure.JwtTokenProvider; @@ -17,8 +19,9 @@ public class LoginService { private final KakaoOauthProvider oauthProvider; private final JwtTokenProvider tokenProvider; - public LoginResponse login(String code) { - OauthUserInformationResponse userInformation = oauthProvider.getUserInformation(code); + public LoginResponse login(String code, String encodedRedirectUri) { + String redirectUri = URLDecoder.decode(encodedRedirectUri, StandardCharsets.UTF_8); + OauthUserInformationResponse userInformation = oauthProvider.getUserInformation(code, redirectUri); Member member = memberRepository.findByKakaoId(userInformation.socialLoginId()) .orElseGet(() -> signUp(userInformation)); diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index 9d2b7c03..a896b3f8 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -37,7 +37,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/swagger-resources/**"), new HttpRequestInfo(HttpMethod.GET, "/v3/api-docs/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), - new HttpRequestInfo(HttpMethod.GET, "/api/v1/login/**"), + new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/shared/**"), new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 85ce7c2f..a54e2b2e 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -3,7 +3,6 @@ oauth: user-information-request-uri: https://kapi.kakao.com/v2/user/me access-token-request-uri: https://kauth.kakao.com/oauth/token rest-api-key: ENC(i6J7NWUsDpXVbJrbSUcNFI3h0oc6v8PxuHShU9UA7EVuUNLtQN/ANII+8j5HjhGO) - redirect-uri: http://localhost:3000/oauth jasypt: encryptor: algorithm: PBEWithMD5AndDES @@ -22,9 +21,6 @@ security: token: secret-key: ENC(SNwFG2NQDZkmIK3nNoZFdwQ0ZxKuoe+qcw10ljdW941YEx/Qky9PEEl+wvAN9S1KR26D3a83SnU=) expire-length: 1800000 -oauth: - kakao: - redirect-uri: http://localhost:8080/api/v1/login/oauth/kakao spring: config: activate: diff --git a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java index f5064936..ea296869 100644 --- a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java @@ -2,7 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -40,10 +40,11 @@ class LoginControllerTest { @Test void loginTest() throws Exception { LoginResponse loginResponse = new LoginResponse("리비", "img-url", "test-access-token"); - when(loginService.login(any(String.class))).thenReturn(loginResponse); + when(loginService.login(any(String.class), any(String.class))).thenReturn(loginResponse); - mockMvc.perform(get("/api/v1/login/oauth/kakao") - .param("code", "test-authorization-code")) + mockMvc.perform(post("/api/v1/login/oauth/kakao") + .param("code", "test-authorization-code") + .param("redirectUri", "https://touroot.kr/oauth")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(objectMapper.writeValueAsString(loginResponse))); diff --git a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java index f3a3c256..0db2ecc4 100644 --- a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java +++ b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java @@ -26,6 +26,7 @@ class LoginServiceTest { private static final String AUTHENTICATION_CODE = "test-authentication-code"; + private static final String REDIRECT_URI = "http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fv1%2Flogin%2Foauth%2Fkakao"; @InjectMocks private LoginService loginService; @@ -40,11 +41,11 @@ class LoginServiceTest { @Test void existUserKakaoSocialLoginTest() { // given - when(kakaoOauthProvider.getUserInformation(AUTHENTICATION_CODE)) + when(kakaoOauthProvider.getUserInformation(any(String.class), any(String.class))) .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); when(memberRepository.findByKakaoId(any(Long.class))) .thenReturn(Optional.of(MemberFixture.MEMBER_1)); - LoginResponse response = loginService.login(AUTHENTICATION_CODE); + LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( @@ -55,13 +56,13 @@ void existUserKakaoSocialLoginTest() { @Test void nonExistUserKakaoSocialLoginTest() { // given - when(kakaoOauthProvider.getUserInformation(AUTHENTICATION_CODE)) + when(kakaoOauthProvider.getUserInformation(any(String.class), any(String.class))) .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); when(memberRepository.findByKakaoId(any(Long.class))) .thenReturn(Optional.empty()); when(memberRepository.save(any(Member.class))) .thenReturn(MemberFixture.MEMBER_1); - LoginResponse response = loginService.login(AUTHENTICATION_CODE); + LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( From 10e20ddde2962668549f8fb307a6e3a5fd7e9ab7 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:09:15 +0900 Subject: [PATCH 073/108] =?UTF-8?q?[Feature]=20-=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20Swagger=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=20(#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행 계획 조회 및 여행기 조회 swagger 문서 추가 * docs: 나의 프로필 정보 조회 swagger 작성 (feat. 리버) --- .../member/controller/MyPageController.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index 9fca1559..69750837 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -1,9 +1,15 @@ package kr.touroot.member.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.NotNull; import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.member.dto.MyTravelPlanResponse; import kr.touroot.member.dto.MyTraveloguesResponse; import kr.touroot.member.dto.ProfileResponse; @@ -22,17 +28,41 @@ @Tag(name = "마이 페이지") @RequiredArgsConstructor @RestController -@RequestMapping("api/v1/member/me") +@RequestMapping("/api/v1/member/me") public class MyPageController { private final MyPageFacadeService myPageFacadeService; + @Operation(summary = "나의 프로필 정보 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "내 프로필 정보 조회에 성공했을 때" + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 프로필 정보 조회에 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) @GetMapping("/profile") public ResponseEntity readProfile(@NotNull MemberAuth memberAuth) { ProfileResponse data = myPageFacadeService.readProfile(memberAuth); return ResponseEntity.ok(data); } + @Operation(summary = "내 여행기 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "내 여행기 조회에 성공했을 때" + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 조회를 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) @PageableAsQueryParam @GetMapping("/travelogues") public ResponseEntity> readTravelogues( @@ -45,6 +75,18 @@ public ResponseEntity> readTravelogues( return ResponseEntity.ok(data); } + @Operation(summary = "내 여행 계획 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "내 여행 계획 조회에 성공했을 때" + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 조회를 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) @PageableAsQueryParam @GetMapping("/travel-plans") public ResponseEntity> readTravelPlans( From d9b7c9876b4f8b99335b0768639ede3083b0a056 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:19:06 +0900 Subject: [PATCH 074/108] =?UTF-8?q?[Feature]=20-=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 마이 페이지 내 여행 계획 조회 response TravelogueResponse로 변경 --- .../member/controller/MyPageController.java | 6 +- .../member/dto/MyTraveloguesResponse.java | 23 ------ .../member/service/MyPageFacadeService.java | 41 ++++++++++- .../controller/MyPageControllerTest.java | 71 +++++++++++++++++++ .../helper/TravelogueTestHelper.java | 20 ++++-- .../controller/TravelPlanControllerTest.java | 11 +-- 6 files changed, 132 insertions(+), 40 deletions(-) delete mode 100644 backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java create mode 100644 backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index 69750837..309aeb39 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -11,9 +11,9 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.member.dto.MyTravelPlanResponse; -import kr.touroot.member.dto.MyTraveloguesResponse; import kr.touroot.member.dto.ProfileResponse; import kr.touroot.member.service.MyPageFacadeService; +import kr.touroot.travelogue.dto.response.TravelogueResponse; import lombok.RequiredArgsConstructor; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -65,13 +65,13 @@ public ResponseEntity readProfile(@NotNull MemberAuth memberAut }) @PageableAsQueryParam @GetMapping("/travelogues") - public ResponseEntity> readTravelogues( + public ResponseEntity> readTravelogues( @NotNull MemberAuth memberAuth, @Parameter(hidden = true) @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable ) { - Page data = myPageFacadeService.readTravelogues(memberAuth, pageable); + Page data = myPageFacadeService.readTravelogues(memberAuth, pageable); return ResponseEntity.ok(data); } diff --git a/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java b/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java deleted file mode 100644 index 7966c8fa..00000000 --- a/backend/src/main/java/kr/touroot/member/dto/MyTraveloguesResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package kr.touroot.member.dto; - -import kr.touroot.travelogue.domain.Travelogue; -import lombok.Builder; - -import java.time.format.DateTimeFormatter; - -@Builder -public record MyTraveloguesResponse(long id, String title, String thumbnailUrl, String createdAt) { - - public static MyTraveloguesResponse from(Travelogue travelogue) { - String createdAt = travelogue.getCreatedAt() - .toLocalDate() - .format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); - - return MyTraveloguesResponse.builder() - .id(travelogue.getId()) - .title(travelogue.getTitle()) - .createdAt(createdAt) - .thumbnailUrl(travelogue.getThumbnail()) - .build(); - } -} diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java index a8fad42f..b9bd723e 100644 --- a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -3,9 +3,16 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.member.domain.Member; import kr.touroot.member.dto.MyTravelPlanResponse; -import kr.touroot.member.dto.MyTraveloguesResponse; import kr.touroot.member.dto.ProfileResponse; import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.dto.response.TravelogueDayResponse; +import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; +import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.service.TravelogueDayService; +import kr.touroot.travelogue.service.TraveloguePhotoService; +import kr.touroot.travelogue.service.TraveloguePlaceService; import kr.touroot.travelogue.service.TravelogueService; import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.service.TravelPlanService; @@ -13,6 +20,9 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; @RequiredArgsConstructor @Service @@ -20,20 +30,45 @@ public class MyPageFacadeService { private final MemberService memberService; private final TravelogueService travelogueService; + private final TravelogueDayService travelogueDayService; + private final TraveloguePlaceService traveloguePlaceService; + private final TraveloguePhotoService traveloguePhotoService; private final TravelPlanService travelPlanService; + @Transactional(readOnly = true) public ProfileResponse readProfile(MemberAuth memberAuth) { Member member = memberService.getById(memberAuth.memberId()); return ProfileResponse.from(member); } - public Page readTravelogues(MemberAuth memberAuth, Pageable pageable) { + @Transactional(readOnly = true) + public Page readTravelogues(MemberAuth memberAuth, Pageable pageable) { Member member = memberService.getById(memberAuth.memberId()); Page travelogues = travelogueService.findAllByMember(member, pageable); - return travelogues.map(MyTraveloguesResponse::from); + return travelogues.map(this::getTravelogueResponse); + } + + private TravelogueResponse getTravelogueResponse(Travelogue travelogue) { + List dayResponses = travelogueDayService.findDaysByTravelogue(travelogue).stream() + .map((this::getTravelogueDayResponse)) + .toList(); + return TravelogueResponse.of(travelogue, dayResponses); + } + + private TravelogueDayResponse getTravelogueDayResponse(TravelogueDay travelogueDay) { + List placeResponses = traveloguePlaceService.findTraveloguePlacesByDay(travelogueDay).stream() + .map(this::getTraveloguePlaceResponse) + .toList(); + return TravelogueDayResponse.of(travelogueDay, placeResponses); + } + + private TraveloguePlaceResponse getTraveloguePlaceResponse(TraveloguePlace traveloguePlace) { + List traveloguePhotos = traveloguePhotoService.findPhotoUrlsByPlace(traveloguePlace); + return TraveloguePlaceResponse.of(traveloguePlace, traveloguePhotos); } + @Transactional(readOnly = true) public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { Member member = memberService.getById(memberAuth.memberId()); Page travelPlans = travelPlanService.getAllByAuthor(member, pageable); diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java new file mode 100644 index 00000000..8573a64f --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -0,0 +1,71 @@ +package kr.touroot.member.controller; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.member.domain.Member; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; + +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("마이 페이지 컨트롤러") +@AcceptanceTest +class MyPageControllerTest { + + private final DatabaseCleaner databaseCleaner; + private final JwtTokenProvider jwtTokenProvider; + private final TravelogueTestHelper testHelper; + + @LocalServerPort + private int port; + private String accessToken; + private Member member; + + @Autowired + public MyPageControllerTest( + DatabaseCleaner databaseCleaner, + JwtTokenProvider jwtTokenProvider, + TravelogueTestHelper testHelper + ) { + this.databaseCleaner = databaseCleaner; + this.jwtTokenProvider = jwtTokenProvider; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + databaseCleaner.executeTruncate(); + + member = testHelper.initMemberTestData(); + accessToken = jwtTokenProvider.createToken(member.getId()); + } + + @DisplayName("마이 페이지 컨트롤러는 내 여행기 조회 요청이 들어오면 로그인한 사용자의 여행기를 조회한다.") + @Test + void readTravelogues() { + // given + testHelper.initTravelogueTestDate(member); + testHelper.initTravelogueTestDate(member); + testHelper.initTravelogueTestData(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .get("/api/v1/member/me/travelogues") + .then().log().all() + .statusCode(200) + .body("content.size()", is(2)); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index a3188b66..541c53ce 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,11 +1,5 @@ package kr.touroot.travelogue.helper; -import static kr.touroot.place.fixture.PlaceFixture.PLACE; -import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; -import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; -import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; -import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; - import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; @@ -22,6 +16,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import static kr.touroot.place.fixture.PlaceFixture.PLACE; +import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; +import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; +import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; +import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; + @Component public class TravelogueTestHelper { @@ -58,6 +58,14 @@ public void initTravelogueTestData() { persistTraveloguePhoto(place); } + public void initTravelogueTestDate(Member author) { + Travelogue travelogue = persistTravelogue(author); + TravelogueDay day = persistTravelogueDay(travelogue); + Place position = persistPlace(); + TraveloguePlace place = persistTraveloguePlace(position, day); + persistTraveloguePhoto(place); + } + public Member persistMember() { Member author = MemberFixture.MEMBER_1; diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index c570499e..ccc60dae 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -1,13 +1,8 @@ package kr.touroot.travelplan.controller; -import static org.hamcrest.Matchers.is; - import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.ContentType; -import java.time.LocalDate; -import java.util.List; -import java.util.UUID; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; @@ -25,6 +20,12 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +import static org.hamcrest.Matchers.is; + @DisplayName("여행 계획 컨트롤러") @AcceptanceTest class TravelPlanControllerTest { From 3291d6c6b74107ebec2a711b0ece66cd4f6689e7 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:06:16 +0900 Subject: [PATCH 075/108] =?UTF-8?q?[Feature]=20-=20=EB=A7=88=EC=9D=B4=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=82=B4=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=9D=91=EB=8B=B5=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?(#233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 내 여행기 조회 응답 원상복귀 * refactor: 마이 페이지 내 여행 계획 조회 response TravelPlanResponse로 변환 --- .../member/controller/MyPageController.java | 12 ++--- .../member/dto/MyTravelogueResponse.java | 23 +++++++++ .../member/service/MyPageFacadeService.java | 48 +++---------------- .../travelplan/service/TravelPlanService.java | 23 +++++---- .../controller/MyPageControllerTest.java | 38 +++++++++++---- .../helper/TravelPlanTestHelper.java | 17 ++++++- 6 files changed, 95 insertions(+), 66 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/member/dto/MyTravelogueResponse.java diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index 309aeb39..7ca42ac8 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -10,10 +10,10 @@ import jakarta.validation.constraints.NotNull; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; -import kr.touroot.member.dto.MyTravelPlanResponse; +import kr.touroot.member.dto.MyTravelogueResponse; import kr.touroot.member.dto.ProfileResponse; import kr.touroot.member.service.MyPageFacadeService; -import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelplan.dto.response.TravelPlanResponse; import lombok.RequiredArgsConstructor; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -65,13 +65,13 @@ public ResponseEntity readProfile(@NotNull MemberAuth memberAut }) @PageableAsQueryParam @GetMapping("/travelogues") - public ResponseEntity> readTravelogues( + public ResponseEntity> readTravelogues( @NotNull MemberAuth memberAuth, @Parameter(hidden = true) @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable ) { - Page data = myPageFacadeService.readTravelogues(memberAuth, pageable); + Page data = myPageFacadeService.readTravelogues(memberAuth, pageable); return ResponseEntity.ok(data); } @@ -89,13 +89,13 @@ public ResponseEntity> readTravelogues( }) @PageableAsQueryParam @GetMapping("/travel-plans") - public ResponseEntity> readTravelPlans( + public ResponseEntity> readTravelPlans( @NotNull MemberAuth memberAuth, @Parameter(hidden = true) @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable ) { - Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); + Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); return ResponseEntity.ok(data); } } diff --git a/backend/src/main/java/kr/touroot/member/dto/MyTravelogueResponse.java b/backend/src/main/java/kr/touroot/member/dto/MyTravelogueResponse.java new file mode 100644 index 00000000..3a0c59b9 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/MyTravelogueResponse.java @@ -0,0 +1,23 @@ +package kr.touroot.member.dto; + +import kr.touroot.travelogue.domain.Travelogue; +import lombok.Builder; + +import java.time.format.DateTimeFormatter; + +@Builder +public record MyTravelogueResponse(long id, String title, String thumbnailUrl, String createdAt) { + + public static MyTravelogueResponse from(Travelogue travelogue) { + String createdAt = travelogue.getCreatedAt() + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + return MyTravelogueResponse.builder() + .id(travelogue.getId()) + .title(travelogue.getTitle()) + .createdAt(createdAt) + .thumbnailUrl(travelogue.getThumbnail()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java index b9bd723e..b32c0fd2 100644 --- a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -2,19 +2,12 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.member.domain.Member; -import kr.touroot.member.dto.MyTravelPlanResponse; +import kr.touroot.member.dto.MyTravelogueResponse; import kr.touroot.member.dto.ProfileResponse; import kr.touroot.travelogue.domain.Travelogue; -import kr.touroot.travelogue.domain.TravelogueDay; -import kr.touroot.travelogue.domain.TraveloguePlace; -import kr.touroot.travelogue.dto.response.TravelogueDayResponse; -import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; -import kr.touroot.travelogue.dto.response.TravelogueResponse; -import kr.touroot.travelogue.service.TravelogueDayService; -import kr.touroot.travelogue.service.TraveloguePhotoService; -import kr.touroot.travelogue.service.TraveloguePlaceService; import kr.touroot.travelogue.service.TravelogueService; import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.dto.response.TravelPlanResponse; import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -22,17 +15,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @RequiredArgsConstructor @Service public class MyPageFacadeService { private final MemberService memberService; private final TravelogueService travelogueService; - private final TravelogueDayService travelogueDayService; - private final TraveloguePlaceService traveloguePlaceService; - private final TraveloguePhotoService traveloguePhotoService; private final TravelPlanService travelPlanService; @Transactional(readOnly = true) @@ -42,42 +30,18 @@ public ProfileResponse readProfile(MemberAuth memberAuth) { } @Transactional(readOnly = true) - public Page readTravelogues(MemberAuth memberAuth, Pageable pageable) { + public Page readTravelogues(MemberAuth memberAuth, Pageable pageable) { Member member = memberService.getById(memberAuth.memberId()); Page travelogues = travelogueService.findAllByMember(member, pageable); - return travelogues.map(this::getTravelogueResponse); - } - - private TravelogueResponse getTravelogueResponse(Travelogue travelogue) { - List dayResponses = travelogueDayService.findDaysByTravelogue(travelogue).stream() - .map((this::getTravelogueDayResponse)) - .toList(); - return TravelogueResponse.of(travelogue, dayResponses); - } - - private TravelogueDayResponse getTravelogueDayResponse(TravelogueDay travelogueDay) { - List placeResponses = traveloguePlaceService.findTraveloguePlacesByDay(travelogueDay).stream() - .map(this::getTraveloguePlaceResponse) - .toList(); - return TravelogueDayResponse.of(travelogueDay, placeResponses); - } - - private TraveloguePlaceResponse getTraveloguePlaceResponse(TraveloguePlace traveloguePlace) { - List traveloguePhotos = traveloguePhotoService.findPhotoUrlsByPlace(traveloguePlace); - return TraveloguePlaceResponse.of(traveloguePlace, traveloguePhotos); + return travelogues.map(MyTravelogueResponse::from); } @Transactional(readOnly = true) - public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { + public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { Member member = memberService.getById(memberAuth.memberId()); Page travelPlans = travelPlanService.getAllByAuthor(member, pageable); - return travelPlans.map(this::getMyTravelPlanResponse); - } - - private MyTravelPlanResponse getMyTravelPlanResponse(TravelPlan travelPlan) { - int period = travelPlanService.calculateTravelPeriod(travelPlan); - return MyTravelPlanResponse.of(travelPlan, period); + return travelPlans.map((travelPlanService::getTravelPlanResponse)); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index bc9e349c..fcbe4c40 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -1,8 +1,5 @@ package kr.touroot.travelplan.service; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; @@ -29,6 +26,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + @RequiredArgsConstructor @Service public class TravelPlanService { @@ -95,6 +96,12 @@ public TravelPlanResponse readTravelPlan(Long planId, MemberAuth memberAuth) { return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } + private void validateAuthor(TravelPlan travelPlan, Member member) { + if (!travelPlan.isAuthor(member)) { + throw new ForbiddenException("여행 계획은 작성자만 조회할 수 있습니다."); + } + } + @Transactional(readOnly = true) public TravelPlanResponse readTravelPlan(UUID shareKey) { TravelPlan travelPlan = getTravelPlanByShareKey(shareKey); @@ -102,12 +109,6 @@ public TravelPlanResponse readTravelPlan(UUID shareKey) { return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } - private void validateAuthor(TravelPlan travelPlan, Member member) { - if (!travelPlan.isAuthor(member)) { - throw new ForbiddenException("여행 계획은 작성자만 조회할 수 있습니다."); - } - } - private TravelPlan getTravelPlanById(Long planId) { return travelPlanRepository.findById(planId) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); @@ -118,6 +119,10 @@ private TravelPlan getTravelPlanByShareKey(UUID shareKey) { .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); } + public TravelPlanResponse getTravelPlanResponse(TravelPlan travelPlan) { + return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + } + private List getTravelPlanDayResponses(TravelPlan travelPlan) { List planDays = travelPlanDayRepository.findByPlan(travelPlan); diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index 8573a64f..b3f5cfec 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -6,6 +6,7 @@ import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.travelplan.helper.TravelPlanTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -15,7 +16,6 @@ import org.springframework.http.HttpHeaders; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; @DisplayName("마이 페이지 컨트롤러") @AcceptanceTest @@ -23,7 +23,8 @@ class MyPageControllerTest { private final DatabaseCleaner databaseCleaner; private final JwtTokenProvider jwtTokenProvider; - private final TravelogueTestHelper testHelper; + private final TravelogueTestHelper travelogueTestHelper; + private final TravelPlanTestHelper travelPlanTestHelper; @LocalServerPort private int port; @@ -34,11 +35,13 @@ class MyPageControllerTest { public MyPageControllerTest( DatabaseCleaner databaseCleaner, JwtTokenProvider jwtTokenProvider, - TravelogueTestHelper testHelper + TravelogueTestHelper travelogueTestHelper, + TravelPlanTestHelper travelPlanTestHelper ) { this.databaseCleaner = databaseCleaner; this.jwtTokenProvider = jwtTokenProvider; - this.testHelper = testHelper; + this.travelogueTestHelper = travelogueTestHelper; + this.travelPlanTestHelper = travelPlanTestHelper; } @BeforeEach @@ -46,7 +49,7 @@ void setUp() { RestAssured.port = port; databaseCleaner.executeTruncate(); - member = testHelper.initMemberTestData(); + member = travelogueTestHelper.initMemberTestData(); accessToken = jwtTokenProvider.createToken(member.getId()); } @@ -54,9 +57,9 @@ void setUp() { @Test void readTravelogues() { // given - testHelper.initTravelogueTestDate(member); - testHelper.initTravelogueTestDate(member); - testHelper.initTravelogueTestData(); + travelogueTestHelper.initTravelogueTestDate(member); + travelogueTestHelper.initTravelogueTestDate(member); + travelogueTestHelper.initTravelogueTestData(); // when & then RestAssured.given().log().all() @@ -68,4 +71,23 @@ void readTravelogues() { .statusCode(200) .body("content.size()", is(2)); } + + @DisplayName("마이 페이지 컨트롤러는 내 여행계획 조회 시 요청이 들어오면 로그인한 사용자의 여행 계획을 조회한다.") + @Test + void readTravelPlans() { + // given + travelPlanTestHelper.initTravelPlanTestData(member); + travelPlanTestHelper.initTravelPlanTestData(member); + travelPlanTestHelper.initTravelPlanTestData(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .get("/api/v1/member/me/travel-plans") + .then().log().all() + .statusCode(200) + .body("content.size()", is(2)); + } } diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 91be7bca..59412561 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -1,6 +1,5 @@ package kr.touroot.travelplan.helper; -import java.util.UUID; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; @@ -15,6 +14,7 @@ import org.springframework.stereotype.Component; import java.time.LocalDate; +import java.util.UUID; @Component public class TravelPlanTestHelper { @@ -60,6 +60,21 @@ public static TravelPlanPlace getTravelPlanPlace(String description, int order, return new TravelPlanPlace(description, order, day, place); } + public TravelPlan initTravelPlanTestData() { + Member author = initMemberTestData(); + TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); + TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); + Place place = getPlace("장소", "37.5175896", "127.0867236", ""); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace("설명", 0, place, travelPlanDay); + + travelPlanRepository.save(travelPlan); + travelPlanDayRepository.save(travelPlanDay); + placeRepository.save(place); + travelPlanPlaceRepository.save(travelPlanPlace); + + return travelPlan; + } + public TravelPlan initTravelPlanTestData(Member author) { TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); From 4f4b45160cba1b5abe0f16a6e36f892e919e192d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:16:12 +0900 Subject: [PATCH 076/108] =?UTF-8?q?[Feature]=20-=20=EC=82=AD=EC=A0=9C=20AP?= =?UTF-8?q?I=20=EA=B5=AC=ED=98=84=20(#235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 도메인 엔티티 soft delete 로 구현 * test: 서비스 테스트 격리를 위해 빠진 데이터 클리너 추가 * feat: 각 리포지토리에 여행기 id로 삭제하는 메소드 추가 * feat: 여행기 삭제 서비스 기능 구현 * feat: 여행기 삭제 API 구현 * feat: 여행계획 도메인 엔티티 soft delete로 구현 * feat: 각 리포지토리에 여행계획 id로 삭제하는 메소드 추가 * feat: 여행계획 삭제 서비스 기능 구현 * feat: 여행계획 삭제 API 구현 * docs: 존재하지 않는 여행기/여행계획 삭제 시 400 응답으로 Swagger 문서 수정 * feat: 존재하지 않는 여행기 삭제시 400을 응답하는 기능 구현 * feat: 존재하지 않는 여행 계획 삭제시 400을 응답하는 기능 구현 * feat: 여행기의 작성자인지 확인하는 기능 구현 * test: 주어진 멤버로 여행기를 저장하는 헬퍼 추가 * feat: 작성자가 아닌 사용자가 여행기를 삭제할 때 403 예외를 발생시키는 기능 구현 * refactor: 삭제 메서드의 매개변수를 엔티티로 수정 * feat: 작성자가 아닌 사용자가 여행계획을 삭제할 때 403 예외를 발생시키는 기능 구현 * test: 실패하는 테스트 코드 수정 * style: 에러 및 에러 메시지 구체화 --------- Co-authored-by: slimsha2dy --- .../controller/TravelogueController.java | 25 ++++++++ .../touroot/travelogue/domain/Travelogue.java | 9 +++ .../travelogue/domain/TravelogueDay.java | 4 ++ .../travelogue/domain/TraveloguePhoto.java | 4 ++ .../travelogue/domain/TraveloguePlace.java | 4 ++ .../repository/TravelogueDayRepository.java | 2 + .../repository/TraveloguePhotoRepository.java | 5 +- .../repository/TraveloguePlaceRepository.java | 5 +- .../service/TravelogueDayService.java | 6 ++ .../service/TravelogueFacadeService.java | 12 ++++ .../service/TraveloguePhotoService.java | 8 ++- .../service/TraveloguePlaceService.java | 7 +++ .../travelogue/service/TravelogueService.java | 12 ++++ .../controller/TravelPlanController.java | 25 ++++++++ .../touroot/travelplan/domain/TravelPlan.java | 4 ++ .../travelplan/domain/TravelPlanDay.java | 4 ++ .../travelplan/domain/TravelPlanPlace.java | 4 ++ .../repository/TravelPlanDayRepository.java | 2 + .../repository/TravelPlanPlaceRepository.java | 3 + .../travelplan/service/TravelPlanService.java | 30 +++++++--- .../controller/TravelogueControllerTest.java | 44 ++++++++++++++ .../helper/TravelogueTestHelper.java | 8 ++- .../service/TravelogueDayServiceTest.java | 21 +++++++ .../service/TravelogueFacadeServiceTest.java | 36 +++++++++++ .../service/TraveloguePhotoServiceTest.java | 33 +++++++++++ .../service/TraveloguePlaceServiceTest.java | 22 +++++++ .../service/TravelogueServiceTest.java | 28 ++++++++- .../controller/TravelPlanControllerTest.java | 59 ++++++++++++++++--- .../service/TravelPlanServiceTest.java | 42 ++++++++++++- 29 files changed, 446 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index ec3a22b1..576fa55f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Sort.Direction; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -97,4 +98,28 @@ public ResponseEntity> findMainPageTravelogues( ) { return ResponseEntity.ok(travelogueFacadeService.findTravelogues(pageable)); } + + @Operation(summary = "여행기 삭제") + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 여행기 ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "작성자가 아닌 사용자가 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteTravelogue(@PathVariable Long id, MemberAuth memberAuth) { + travelogueFacadeService.deleteTravelogueById(id, memberAuth); + return ResponseEntity.noContent() + .build(); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index 66028950..f60f4e6f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -9,6 +9,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import java.net.URL; +import java.util.Objects; import kr.touroot.global.entity.BaseEntity; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; @@ -16,10 +17,14 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travelogue SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class Travelogue extends BaseEntity { @@ -84,4 +89,8 @@ private void validateThumbnailFormat(String thumbnailUrl) { throw new BadRequestException("이미지 url 형식이 잘못되었습니다"); } } + + public boolean isAuthor(Member author) { + return Objects.equals(author.getId(), this.author.getId()); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java index fe3c15df..256abbdd 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueDay.java @@ -14,10 +14,14 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travelogue_day SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class TravelogueDay extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java index 39aaf6e1..62acc000 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePhoto.java @@ -14,10 +14,14 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travelogue_photo SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class TraveloguePhoto extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index 398caae0..1385a258 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -15,10 +15,14 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travelogue_place SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class TraveloguePlace extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java index ef5f2f46..0e492f58 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java @@ -8,4 +8,6 @@ public interface TravelogueDayRepository extends JpaRepository { List findByTravelogue(Travelogue travelogue); + + void deleteByTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java index cabb5f95..16de240b 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -1,11 +1,14 @@ package kr.touroot.travelogue.repository; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; +import org.springframework.data.jpa.repository.JpaRepository; public interface TraveloguePhotoRepository extends JpaRepository { List findByTraveloguePlace(TraveloguePlace traveloguePlace); + + void deleteByTraveloguePlaceTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java index 2eb48f46..49de742f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java @@ -1,11 +1,14 @@ package kr.touroot.travelogue.repository; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePlace; +import org.springframework.data.jpa.repository.JpaRepository; public interface TraveloguePlaceRepository extends JpaRepository { List findByTravelogueDay(TravelogueDay travelogueDay); + + void deleteByTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java index 38ac5532..f1b5957c 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java @@ -11,6 +11,7 @@ import kr.touroot.travelogue.repository.TravelogueDayRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -41,4 +42,9 @@ public TravelogueDay findDayById(Long id) { return travelogueDayRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기 일자입니다.")); } + + @Transactional + public void deleteByTravelogue(Travelogue travelogue) { + travelogueDayRepository.deleteByTravelogue(travelogue); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index 9dd77878..61a1b25a 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -105,4 +105,16 @@ private List findPlacesOfTravelogueDay(TravelogueDay tr private List findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) { return traveloguePhotoService.findPhotoUrlsByPlace(place); } + + @Transactional + public void deleteTravelogueById(Long id, MemberAuth member) { + Member author = memberService.getById(member.memberId()); + Travelogue travelogue = travelogueService.getTravelogueById(id); + travelogueService.validateDeleteByAuthor(travelogue, author); + + traveloguePhotoService.deleteByTravelogue(travelogue); + traveloguePlaceService.deleteByTravelogue(travelogue); + travelogueDayService.deleteByTravelogue(travelogue); + travelogueService.delete(travelogue, author); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java index 11b2d277..5a1f18d5 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java @@ -4,13 +4,14 @@ import java.util.Comparator; import java.util.List; import kr.touroot.image.infrastructure.AwsS3Provider; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import software.amazon.awssdk.services.s3.S3Client; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -40,4 +41,9 @@ public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { .map(TraveloguePhoto::getKey) .toList(); } + + @Transactional + public void deleteByTravelogue(Travelogue travelogue) { + traveloguePhotoRepository.deleteByTraveloguePlaceTravelogueDayTravelogue(travelogue); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java index e2442f2d..41c1ddc6 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java @@ -6,6 +6,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.place.domain.Place; import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; @@ -13,6 +14,7 @@ import kr.touroot.travelogue.repository.TraveloguePlaceRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -54,4 +56,9 @@ public TraveloguePlace findTraveloguePlaceById(Long id) { return traveloguePlaceRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기 장소입니다.")); } + + @Transactional + public void deleteByTravelogue(Travelogue travelogue) { + traveloguePlaceRepository.deleteByTravelogueDayTravelogue(travelogue); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index 352a7004..e3701b64 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -1,6 +1,7 @@ package kr.touroot.travelogue.service; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; @@ -36,4 +37,15 @@ public Page findAll(Pageable pageable) { public Page findAllByMember(Member member, Pageable pageable) { return travelogueRepository.findAllByAuthor(member, pageable); } + + public void delete(Travelogue travelogue, Member author) { + validateDeleteByAuthor(travelogue, author); + travelogueRepository.delete(travelogue); + } + + public void validateDeleteByAuthor(Travelogue travelogue, Member author) { + if (!travelogue.isAuthor(author)) { + throw new ForbiddenException("여행기 삭제는 작성자만 가능합니다."); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java index 2810b101..29fcd8ec 100644 --- a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java @@ -18,6 +18,7 @@ import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -86,6 +87,30 @@ public ResponseEntity readTravelPlan( return ResponseEntity.ok(data); } + @Operation(summary = "여행 계획 삭제") + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 여행 계획 ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "작성자가 아닌 사용자가 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteTravelPlan(@PathVariable Long id, MemberAuth memberAuth) { + travelPlanService.deleteByTravelPlanId(id, memberAuth); + return ResponseEntity.noContent() + .build(); + } + @Operation(summary = "공유된 여행 계획 상세 조회") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 03e6091a..86cdfcbb 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -16,9 +16,13 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travel_plan SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class TravelPlan extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java index 3ed5aa21..7b69924c 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanDay.java @@ -14,9 +14,13 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travel_plan_day SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class TravelPlanDay extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index 2ceeb606..44b6ed6b 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -14,9 +14,13 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travel_plan_place SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") @Entity public class TravelPlanPlace extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java index f5e0d0e2..d91444af 100644 --- a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanDayRepository.java @@ -8,4 +8,6 @@ public interface TravelPlanDayRepository extends JpaRepository { List findByPlan(TravelPlan travelPlan); + + void deleteByPlan(TravelPlan plan); } diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java index c7b5bca3..36f07fc9 100644 --- a/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelplan/repository/TravelPlanPlaceRepository.java @@ -1,6 +1,7 @@ package kr.touroot.travelplan.repository; import java.util.List; +import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.domain.TravelPlanDay; import kr.touroot.travelplan.domain.TravelPlanPlace; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,4 +9,6 @@ public interface TravelPlanPlaceRepository extends JpaRepository { List findByDay(TravelPlanDay day); + + void deleteByDayPlan(TravelPlan plan); } diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index fcbe4c40..640dc8c5 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -1,5 +1,8 @@ package kr.touroot.travelplan.service; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; @@ -26,10 +29,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; - @RequiredArgsConstructor @Service public class TravelPlanService { @@ -91,14 +90,14 @@ private Place getPlace(PlanPlaceCreateRequest planRequest) { public TravelPlanResponse readTravelPlan(Long planId, MemberAuth memberAuth) { TravelPlan travelPlan = getTravelPlanById(planId); Member member = getMemberByMemberAuth(memberAuth); - validateAuthor(travelPlan, member); + validateReadByAuthor(travelPlan, member); return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } - private void validateAuthor(TravelPlan travelPlan, Member member) { + private void validateReadByAuthor(TravelPlan travelPlan, Member member) { if (!travelPlan.isAuthor(member)) { - throw new ForbiddenException("여행 계획은 작성자만 조회할 수 있습니다."); + throw new ForbiddenException("여행 계획 조회는 작성자만 가능합니다."); } } @@ -149,4 +148,21 @@ public int calculateTravelPeriod(TravelPlan travelPlan) { return travelPlanDayRepository.findByPlan(travelPlan) .size(); } + + @Transactional + public void deleteByTravelPlanId(Long planId, MemberAuth memberAuth) { + TravelPlan travelPlan = getTravelPlanById(planId); + Member author = getMemberByMemberAuth(memberAuth); + validateDeleteByAuthor(travelPlan, author); + + travelPlanPlaceRepository.deleteByDayPlan(travelPlan); + travelPlanDayRepository.deleteByPlan(travelPlan); + travelPlanRepository.delete(travelPlan); + } + + private void validateDeleteByAuthor(TravelPlan travelPlan, Member member) { + if (!travelPlan.isAuthor(member)) { + throw new ForbiddenException("여행 계획 삭제는 작성자만 가능합니다."); + } + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index a4e8d084..0b207043 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -11,6 +11,7 @@ import kr.touroot.global.AcceptanceTest; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; @@ -133,4 +134,47 @@ void findNotExistTravelogueThrowException() { .statusCode(400).assertThat() .body("message", is("존재하지 않는 여행기입니다.")); } + + @DisplayName("여행기를 삭제한다.") + @Test + void deleteTravelogue() { + Member member = testHelper.initMemberTestData(); + testHelper.initTravelogueTestData(member); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travelogues/1") + .then().log().all() + .statusCode(204); + } + + @DisplayName("존재하지 않는 여행기 삭제시 400를 응답한다.") + @Test + void deleteTravelogueWithNonExist() { + Member member = testHelper.initMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travelogues/1") + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행기입니다.")); + } + + @DisplayName("작성자가 아닌 사용자가 여행기 삭제시 403을 응답한다.") + @Test + void deleteTravelogueWithNotAuthor() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + Member notAuthor = testHelper.initMemberTestData(); + String accessToken = jwtTokenProvider.createToken(notAuthor.getId()); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travelogues/" + travelogue.getId()) + .then().log().all() + .statusCode(403) + .body("message", is("여행기 삭제는 작성자만 가능합니다.")); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 541c53ce..38a8f1ac 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -49,13 +49,19 @@ public TravelogueTestHelper( this.memberRepository = memberRepository; } - public void initTravelogueTestData() { + public Travelogue initTravelogueTestData() { Member author = persistMember(); + return initTravelogueTestData(author); + } + + public Travelogue initTravelogueTestData(Member author) { Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); Place position = persistPlace(); TraveloguePlace place = persistTraveloguePlace(position, day); persistTraveloguePhoto(place); + + return travelogue; } public void initTravelogueTestDate(Member author) { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java index f3b4e4b0..22961ae1 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -16,6 +16,7 @@ import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.travelogue.repository.TravelogueDayRepository; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -29,16 +30,19 @@ class TravelogueDayServiceTest { private final TravelogueDayService dayService; + private final TravelogueDayRepository dayRepository; private final DatabaseCleaner databaseCleaner; private final TravelogueTestHelper testHelper; @Autowired public TravelogueDayServiceTest( TravelogueDayService dayService, + TravelogueDayRepository dayRepository, DatabaseCleaner databaseCleaner, TravelogueTestHelper testHelper ) { this.dayService = dayService; + this.dayRepository = dayRepository; this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; } @@ -91,4 +95,21 @@ void findDayByInvalidIdThrowException() { .isInstanceOf(BadRequestException.class) .hasMessage("존재하지 않는 여행기 일자입니다."); } + + @DisplayName("주어진 여행기의 여행기 일자를 삭제할 수 있다.") + @Test + void deleteTravelogueDayById() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + long travelogueId = travelogue.getId(); + dayService.deleteByTravelogue(travelogue); + + assertThat(dayRepository.findAll() + .stream() + .noneMatch(day -> extractTravelogue(day).getId() == travelogueId)) + .isTrue(); + } + + private Travelogue extractTravelogue(TravelogueDay day) { + return day.getTravelogue(); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index d3b70dfb..9dc4d3f1 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -1,10 +1,13 @@ package kr.touroot.travelogue.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.service.MemberService; import kr.touroot.travelogue.dto.request.TravelogueRequest; @@ -97,4 +100,37 @@ void findTravelogues() { assertThat(service.findTravelogues(Pageable.ofSize(5))) .isEqualTo(responses); } + + @DisplayName("여행기를 ID를 기준으로 삭제한다.") + @Test + void deleteById() { + testHelper.initTravelogueTestData(); + MemberAuth memberAuth = new MemberAuth(1L); + service.deleteTravelogueById(1L, memberAuth); + + assertThatThrownBy(() -> service.findTravelogueById(1L)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + + @DisplayName("존재하지 않는 ID로 여행기를 삭제하면 예외가 발생한다.") + @Test + void deleteTravelogueByNotExistsIdThrowException() { + MemberAuth memberAuth = new MemberAuth(testHelper.initMemberTestData().getId()); + + assertThatThrownBy(() -> service.deleteTravelogueById(1L, memberAuth)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + + @DisplayName("작성자가 아닌 사용자가 여행기를 삭제하면 예외가 발생한다.") + @Test + void deleteByIdWithNotAuthor() { + testHelper.initTravelogueTestData(); + MemberAuth notAuthorAuth = new MemberAuth(testHelper.initMemberTestData().getId()); + + assertThatThrownBy(() -> service.deleteTravelogueById(1L, notAuthorAuth)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("여행기 삭제는 작성자만 가능합니다."); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java index 4cf3b6bc..ba2de820 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -15,6 +15,9 @@ import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.travelogue.repository.TraveloguePhotoRepository; +import kr.touroot.utils.DatabaseCleaner; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -28,21 +31,32 @@ class TraveloguePhotoServiceTest { private final TraveloguePhotoService photoService; + private final TraveloguePhotoRepository photoRepository; private final TravelogueTestHelper testHelper; + private final DatabaseCleaner databaseCleaner; @MockBean private final AwsS3Provider s3Provider; @Autowired public TraveloguePhotoServiceTest( TraveloguePhotoService photoService, + TraveloguePhotoRepository photoRepository, TravelogueTestHelper testHelper, + DatabaseCleaner databaseCleaner, AwsS3Provider s3Provider ) { this.photoService = photoService; + this.photoRepository = photoRepository; this.testHelper = testHelper; + this.databaseCleaner = databaseCleaner; this.s3Provider = s3Provider; } + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + @DisplayName("여행기 사진을 생성한다.") @Test void createPhotos() { @@ -75,4 +89,23 @@ void findPhotoUrlsByPlace() { assertThat(photoUrls).contains(photo.getKey()); } + + @DisplayName("주어진 여행기의 여행기 사진을 삭제할 수 있다.") + @Test + void deleteTraveloguePhotoById() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + long travelogueId = travelogue.getId(); + photoService.deleteByTravelogue(travelogue); + + assertThat(photoRepository.findAll() + .stream() + .noneMatch(photo -> extractTravelogue(photo).getId() == travelogueId)) + .isTrue(); + } + + private Travelogue extractTravelogue(TraveloguePhoto photo) { + return photo.getTraveloguePlace() + .getTravelogueDay() + .getTravelogue(); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java index f70525ce..3504abc3 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -18,6 +18,7 @@ import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.travelogue.repository.TraveloguePlaceRepository; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -31,16 +32,19 @@ class TraveloguePlaceServiceTest { private final TraveloguePlaceService placeService; + private final TraveloguePlaceRepository placeRepository; private final DatabaseCleaner databaseCleaner; private final TravelogueTestHelper testHelper; @Autowired public TraveloguePlaceServiceTest( TraveloguePlaceService placeService, + TraveloguePlaceRepository placeRepository, DatabaseCleaner databaseCleaner, TravelogueTestHelper testHelper ) { this.placeService = placeService; + this.placeRepository = placeRepository; this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; } @@ -96,4 +100,22 @@ void findDayByInvalidIdThrowException() { .isInstanceOf(BadRequestException.class) .hasMessage("존재하지 않는 여행기 장소입니다."); } + + @DisplayName("주어진 여행기의 여행기 장소를 삭제할 수 있다.") + @Test + void deleteTraveloguePlaceById() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + long travelogueId = travelogue.getId(); + placeService.deleteByTravelogue(travelogue); + + assertThat(placeRepository.findAll() + .stream() + .noneMatch(place -> extractTravelogue(place).getId() == travelogueId)) + .isTrue(); + } + + private Travelogue extractTravelogue(TraveloguePlace place) { + return place.getTravelogueDay() + .getTravelogue(); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index b356ff70..4134bde1 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -8,6 +8,7 @@ import kr.touroot.global.ServiceTest; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; @@ -62,7 +63,7 @@ void createTravelogue() { Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); - Member author = testHelper.persistMember(); + Member author = testHelper.initMemberTestData(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); Travelogue createdTravelogue = travelogueService.createTravelogue(author, request); @@ -97,4 +98,29 @@ void findAll() { assertThat(travelogueService.findAll(Pageable.ofSize(BASIC_PAGE_SIZE))) .hasSize(1); } + + @DisplayName("여행기를 삭제할 수 있다.") + @Test + void deleteTravelogueById() { + Member author = testHelper.initMemberTestData(); + Travelogue travelogue = testHelper.initTravelogueTestData(author); + long travelogueId = travelogue.getId(); + + travelogueService.delete(travelogue, author); + + assertThatThrownBy(() -> travelogueService.getTravelogueById(travelogueId)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + + @DisplayName("작성자가 아닌 사람이 여행기를 삭제하면 예외가 발생한다.") + @Test + void deleteTravelogueByNotAuthorThrowException() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + Member notAuthor = testHelper.initMemberTestData(); + + assertThatThrownBy(() -> travelogueService.delete(travelogue, notAuthor)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("여행기 삭제는 작성자만 가능합니다."); + } } diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index ccc60dae..95c18ba5 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -1,8 +1,13 @@ package kr.touroot.travelplan.controller; +import static org.hamcrest.Matchers.is; + import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; @@ -20,22 +25,16 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; -import java.time.LocalDate; -import java.util.List; -import java.util.UUID; - -import static org.hamcrest.Matchers.is; - @DisplayName("여행 계획 컨트롤러") @AcceptanceTest class TravelPlanControllerTest { - @LocalServerPort - private int port; private final ObjectMapper objectMapper; private final DatabaseCleaner databaseCleaner; private final JwtTokenProvider jwtTokenProvider; private final TravelPlanTestHelper testHelper; + @LocalServerPort + private int port; private String accessToken; private Member member; @@ -170,7 +169,7 @@ void readTravelPlanWithNotAuthor() { .get("/api/v1/travel-plans/" + id) .then().log().all() .statusCode(403) - .body("message", is("여행 계획은 작성자만 조회할 수 있습니다.")); + .body("message", is("여행 계획 조회는 작성자만 가능합니다.")); } @DisplayName("여행 계획 공유 키를 통해 여행 계획을 조회할 수 있다") @@ -244,4 +243,46 @@ void readTravelPlanByInvalidShareKey() { .statusCode(400) .body("message", is("존재하지 않는 여행 계획입니다.")); } + + @DisplayName("여행계획을 삭제한다.") + @Test + void deleteTravelPlan() { + long id = testHelper.initTravelPlanTestData(member).getId(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(204); + } + + @DisplayName("존재하지 않는 여행 계획 삭제시 400를 응답한다.") + @Test + void deleteTravelPlanWithNonExist() { + long id = 1L; + String accessToken = jwtTokenProvider.createToken(member.getId()); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행 계획입니다.")); + } + + @DisplayName("작성자가 아닌 사용자가 여행 계획 삭제시 403을 응답한다.") + @Test + void deleteTravelPlanWithNotAuthor() { + long id = testHelper.initTravelPlanTestData(member).getId(); + Member notAuthor = testHelper.initMemberTestData(); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + notAuthorAccessToken) + .when().delete("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(403) + .body("message", is("여행 계획 삭제는 작성자만 가능합니다.")); + } } diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index d277e113..b4e4b9ad 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -19,6 +19,7 @@ import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; import kr.touroot.travelplan.dto.response.TravelPlanResponse; import kr.touroot.travelplan.helper.TravelPlanTestHelper; +import kr.touroot.travelplan.repository.TravelPlanRepository; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ class TravelPlanServiceTest { private final TravelPlanService travelPlanService; + private final TravelPlanRepository travelPlanRepository; private final DatabaseCleaner databaseCleaner; private final TravelPlanTestHelper testHelper; @@ -41,10 +43,12 @@ class TravelPlanServiceTest { @Autowired public TravelPlanServiceTest( TravelPlanService travelPlanService, + TravelPlanRepository travelPlanRepository, DatabaseCleaner databaseCleaner, TravelPlanTestHelper testHelper ) { this.travelPlanService = travelPlanService; + this.travelPlanRepository = travelPlanRepository; this.databaseCleaner = databaseCleaner; this.testHelper = testHelper; } @@ -140,7 +144,7 @@ void readTravelPlanWithNotAuthor() { // when & then assertThatThrownBy(() -> travelPlanService.readTravelPlan(id, notAuthor)) .isInstanceOf(ForbiddenException.class) - .hasMessage("여행 계획은 작성자만 조회할 수 있습니다."); + .hasMessage("여행 계획 조회는 작성자만 가능합니다."); } @DisplayName("여행 계획 서비스는 여행 계획 일자를 계산해 반환한다.") @@ -156,6 +160,42 @@ void calculateTravelPeriod() { assertThat(actual).isEqualTo(1); } + @DisplayName("여행계획을 ID 기준으로 삭제할 수 있다.") + @Test + void deleteTravelPlanById() { + TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); + travelPlanService.deleteByTravelPlanId(travelPlan.getId(), memberAuth); + + assertThat(travelPlanRepository.findById(travelPlan.getId())) + .isEmpty(); + } + + @DisplayName("여행 계획 서비스는 존재하지 않는 여행 계획 삭제 시 예외를 반환한다.") + @Test + void deleteTravelPlanWitNonExist() { + // given + databaseCleaner.executeTruncate(); + Long id = 1L; + + // when & then + assertThatThrownBy(() -> travelPlanService.deleteByTravelPlanId(id, memberAuth)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행 계획입니다."); + } + + @DisplayName("여행 계획 서비스는 작성자가 아닌 사용자가 삭제 시 예외를 반환한다.") + @Test + void deleteTravelPlanWithNotAuthor() { + // given + Long id = testHelper.initTravelPlanTestData(author).getId(); + MemberAuth notAuthor = new MemberAuth(testHelper.initMemberTestData().getId()); + + // when & then + assertThatThrownBy(() -> travelPlanService.deleteByTravelPlanId(id, notAuthor)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("여행 계획 삭제는 작성자만 가능합니다."); + } + @DisplayName("여행 계획 서비스는 공유 키로 여행 계획을 조회할 수 있다") @Test void readTravelPlanByShareKey() { From f52ed012d6fccdb246ee644e701a1b223d9453bf Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 7 Aug 2024 11:37:40 +0900 Subject: [PATCH 077/108] =?UTF-8?q?[Feature]=20-=20=EC=9E=90=EC=B2=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B0=80=EC=9E=85=EA=B3=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84=20(#234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 투룻 서비스 자체 로그인 API 작성 * feat: 회원 가입 및 로그인 기능 구현 * feat: 회원의 로그인 타입에 따른 구분을 위한 열거형 추가 * feat: 회원 가입 기능 구현 * feat: 회원 가입 및 로그인 기능 토큰 필터에서 제외 * feat: 이미지 URL 검증 추가에 따른 Swagger example 수정 * refactor: URL 검증 예외 메시지 수정 * feat: 로그인 요청 DTO 구현 * refactor: 회원 가입 및 로그인 기능 구현에 따른 테스트 코드 수정 * chore: 반환 URI 수정 * refactor: 이메일과 비밀번호 null 검증 추가 * refactor: 사용자 도메인 테스트 추가 작성 * refactor: 사용자 test fixture 추가 * refactor: Test fixture 이름 변경에 따른 리팩토링 * test: 사용자 회원 가입 로직 컨트롤러 계층과 서비스 계층 통합 테스트 작성 * fix: Conflict 해결 과정에서 누락된 수정 사항 반영 * fix: Conflict 해결 과정에서 누락된 수정 사항 반영 * refactor: 불필요한 요청 화이트리스트 제거 * refactor: test fixture lombok을 활용한 enum fixture로 개선 * fix: 변경된 메소드 이름이 반영되지 않은 코드 수정 --- backend/build.gradle | 2 + .../controller/LoginController.java | 23 +++- .../dto/request/LoginRequest.java | 16 +++ .../OauthUserInformationResponse.java | 3 +- .../infrastructure/PasswordEncryptor.java | 25 ++++ .../authentication/service/LoginService.java | 12 ++ .../kr/touroot/global/auth/JwtAuthFilter.java | 1 + .../image/infrastructure/AwsS3Provider.java | 2 +- .../member/controller/MemberController.java | 48 ++++++++ .../kr/touroot/member/domain/LoginType.java | 5 + .../java/kr/touroot/member/domain/Member.java | 57 +++++++-- .../member/dto/request/MemberRequest.java | 28 +++++ .../member/repository/MemberRepository.java | 8 +- .../touroot/member/service/MemberService.java | 28 +++++ .../dto/request/TraveloguePhotoRequest.java | 4 +- .../dto/request/TravelogueRequest.java | 2 +- .../authentication/fixture/MemberFixture.java | 12 +- .../service/LoginServiceTest.java | 10 +- .../infrastructure/AwsS3ProviderTest.java | 14 +-- .../controller/MemberControllerTest.java | 111 ++++++++++++++++++ .../controller/MyPageControllerTest.java | 6 +- .../kr/touroot/member/domain/MemberTest.java | 58 +++++++-- .../member/fixture/MemberRequestFixture.java | 34 ++++++ .../member/helper/MemberTestHelper.java | 25 ++++ .../member/service/MemberServiceTest.java | 94 +++++++++++++++ .../touroot/place/fixture/PlaceFixture.java | 9 +- .../controller/TravelogueControllerTest.java | 8 +- .../travelogue/domain/TravelogueDayTest.java | 4 +- .../travelogue/domain/TravelogueTest.java | 4 +- .../fixture/TravelogueDayFixture.java | 7 +- .../travelogue/fixture/TravelogueFixture.java | 13 +- .../fixture/TraveloguePhotoFixture.java | 8 +- .../fixture/TraveloguePlaceFixture.java | 9 +- .../helper/TravelogueTestHelper.java | 21 ++-- .../service/TravelogueFacadeServiceTest.java | 8 +- .../service/TravelogueServiceTest.java | 6 +- .../travelplan/domain/TravelPlanTest.java | 9 +- .../fixture/TravelPlanDayFixture.java | 7 +- .../travelplan/fixture/TravelPlanFixture.java | 13 +- .../helper/TravelPlanTestHelper.java | 12 +- 40 files changed, 631 insertions(+), 135 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/authentication/dto/request/LoginRequest.java create mode 100644 backend/src/main/java/kr/touroot/authentication/infrastructure/PasswordEncryptor.java create mode 100644 backend/src/main/java/kr/touroot/member/controller/MemberController.java create mode 100644 backend/src/main/java/kr/touroot/member/domain/LoginType.java create mode 100644 backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java create mode 100644 backend/src/test/java/kr/touroot/member/controller/MemberControllerTest.java create mode 100644 backend/src/test/java/kr/touroot/member/fixture/MemberRequestFixture.java create mode 100644 backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java create mode 100644 backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java diff --git a/backend/build.gradle b/backend/build.gradle index 3aa25d0a..847fe90c 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -44,6 +44,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.5.0' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java index 066785f2..12b8e6e0 100644 --- a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java @@ -6,12 +6,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import kr.touroot.authentication.dto.request.LoginRequest; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.service.LoginService; import kr.touroot.global.exception.dto.ExceptionResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -37,11 +40,29 @@ public class LoginController { ) }) @PostMapping("/oauth/kakao") - public ResponseEntity login( + public ResponseEntity kakaoLogin( @RequestParam(name = "code") String authorizationCode, @RequestParam(name = "redirectUri") String encodedRedirectUri ) { return ResponseEntity.ok() .body(loginService.login(authorizationCode, encodedRedirectUri)); } + + @Operation(summary = "투룻 서비스 자체 로그인") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 이메일 또는 비밀번호가 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PostMapping + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + return ResponseEntity.ok() + .body(loginService.login(request)); + } } diff --git a/backend/src/main/java/kr/touroot/authentication/dto/request/LoginRequest.java b/backend/src/main/java/kr/touroot/authentication/dto/request/LoginRequest.java new file mode 100644 index 00000000..2cbde17b --- /dev/null +++ b/backend/src/main/java/kr/touroot/authentication/dto/request/LoginRequest.java @@ -0,0 +1,16 @@ +package kr.touroot.authentication.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @Schema(description = "사용자 이메일", example = "email@gmail.com") + @NotBlank(message = "이메일은 비어있을 수 없습니다.") + @Email + String email, + @Schema(description = "사용자 비밀번호", example = "@testpassword1234") + @NotBlank(message = "비밀번호는 비어있을 수 없습니다.") + String password +) { +} diff --git a/backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java index ceab39de..e693f2ac 100644 --- a/backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/OauthUserInformationResponse.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import kr.touroot.authentication.dto.response.kakao.KakaoAccount; +import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; public record OauthUserInformationResponse( @@ -12,7 +13,7 @@ public record OauthUserInformationResponse( ) { public Member toMember() { - return new Member(socialLoginId, nickname(), profileImage()); + return new Member(socialLoginId, nickname(), profileImage(), LoginType.KAKAO); } public String nickname() { diff --git a/backend/src/main/java/kr/touroot/authentication/infrastructure/PasswordEncryptor.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/PasswordEncryptor.java new file mode 100644 index 00000000..2047051f --- /dev/null +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/PasswordEncryptor.java @@ -0,0 +1,25 @@ +package kr.touroot.authentication.infrastructure; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.springframework.stereotype.Component; + +@Component +public class PasswordEncryptor { + + public static final int HEXADECIMAL = 16; + + public String encrypt(String password) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + + byte[] message = md.digest(password.getBytes()); + BigInteger number = new BigInteger(1, message); + + return number.toString(HEXADECIMAL); + } catch (NoSuchAlgorithmException exception) { + throw new RuntimeException(); + } + } +} diff --git a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java index 0999c89c..53f7c6f2 100644 --- a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java @@ -1,11 +1,14 @@ package kr.touroot.authentication.service; +import kr.touroot.authentication.dto.request.LoginRequest; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.dto.response.OauthUserInformationResponse; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.authentication.infrastructure.KakaoOauthProvider; +import kr.touroot.authentication.infrastructure.PasswordEncryptor; +import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -18,6 +21,7 @@ public class LoginService { private final MemberRepository memberRepository; private final KakaoOauthProvider oauthProvider; private final JwtTokenProvider tokenProvider; + private final PasswordEncryptor passwordEncryptor; public LoginResponse login(String code, String encodedRedirectUri) { String redirectUri = URLDecoder.decode(encodedRedirectUri, StandardCharsets.UTF_8); @@ -31,4 +35,12 @@ public LoginResponse login(String code, String encodedRedirectUri) { private Member signUp(OauthUserInformationResponse userInformation) { return memberRepository.save(userInformation.toMember()); } + + public LoginResponse login(LoginRequest request) { + String encryptPassword = passwordEncryptor.encrypt(request.password()); + Member member = memberRepository.findByEmailAndPassword(request.email(), encryptPassword) + .orElseThrow(() -> new BadRequestException("잘못된 이메일 또는 비밀번호입니다.")); + + return LoginResponse.of(member, tokenProvider.createToken(member.getId())); + } } diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index a896b3f8..e3c20aac 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -39,6 +39,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/shared/**"), + new HttpRequestInfo(HttpMethod.POST, "/api/v1/members"), new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); diff --git a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java index 79df1ead..d4b9e374 100644 --- a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java @@ -95,7 +95,7 @@ public String copyImageToPermanentStorage(String imageUrl) { private void validateS3Path(String imageKey) { if (!imageKey.startsWith(imageBaseUri + temporaryStoragePath)) { - throw new BadRequestException("이미지 url 형식이 잘못되었습니다."); + throw new BadRequestException("S3 이미지 url 형식이 잘못되었습니다."); } } diff --git a/backend/src/main/java/kr/touroot/member/controller/MemberController.java b/backend/src/main/java/kr/touroot/member/controller/MemberController.java new file mode 100644 index 00000000..5668f071 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/controller/MemberController.java @@ -0,0 +1,48 @@ +package kr.touroot.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.net.URI; +import kr.touroot.global.exception.dto.ExceptionResponse; +import kr.touroot.member.dto.request.MemberRequest; +import kr.touroot.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "사용자") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/members") +public class MemberController { + + private final MemberService memberService; + + @Operation(summary = "회원 가입") + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 값이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @PostMapping + public ResponseEntity createMember(@Valid @RequestBody MemberRequest request) { + Long id = memberService.createMember(request); + + return ResponseEntity.created(URI.create("/api/v1/members/" + id)) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/member/domain/LoginType.java b/backend/src/main/java/kr/touroot/member/domain/LoginType.java new file mode 100644 index 00000000..afd7f3de --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/domain/LoginType.java @@ -0,0 +1,5 @@ +package kr.touroot.member.domain; + +public enum LoginType { + KAKAO, DEFAULT +} diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index 0908d8e1..038a7840 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -2,6 +2,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -24,37 +26,70 @@ public class Member extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) private Long kakaoId; + private String email; + + private String password; + @Column(nullable = false) private String nickname; @Column(nullable = false) private String profileImageUrl; - public Member(Long id, Long kakaoId, String nickname, String profileImageUrl) { - validate(kakaoId, nickname, profileImageUrl); + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private LoginType loginType; + + public Member( + Long id, Long kakaoId, String email, String password, String nickname, String url, LoginType loginType + ) { + validate(kakaoId, email, password, nickname, url, loginType); this.id = id; this.kakaoId = kakaoId; + this.email = email; + this.password = password; this.nickname = nickname; - this.profileImageUrl = profileImageUrl; + this.profileImageUrl = url; + this.loginType = loginType; + } + + public Member(Long kakaoId, String nickname, String profileImageUrl, LoginType loginType) { + this(null, kakaoId, null, null, nickname, profileImageUrl, loginType); } - public Member(Long kakaoId, String nickname, String profileImageUrl) { - this(null, kakaoId, nickname, profileImageUrl); + public Member(String email, String password, String nickname, String profileImageUrl, LoginType loginType) { + this(null, null, email, password, nickname, profileImageUrl, loginType); } - private void validate(Long kakaoId, String nickname, String profileImageUrl) { - validateNotNull(kakaoId, nickname, profileImageUrl); + private void validate( + Long kakaoId, String email, String password, String nickname, String profileImageUrl, LoginType loginType + ) { + validateByLoginType(kakaoId, email, password, loginType); + validateNotNull(nickname, profileImageUrl); validateNotBlank(nickname, profileImageUrl); validateNicknameLength(nickname); validateProfileImageUrl(profileImageUrl); } - private void validateNotNull(Long kakaoId, String nickname, String profileImageUrl) { - if (kakaoId == null || nickname == null || profileImageUrl == null) { - throw new BadRequestException("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + private void validateByLoginType(Long kakaoId, String email, String password, LoginType loginType) { + if (loginType.equals(LoginType.KAKAO) && kakaoId == null) { + throw new BadRequestException("카카오 ID는 비어 있을 수 없습니다"); + } + + if (loginType.equals(LoginType.DEFAULT) && (email == null || password == null)) { + throw new BadRequestException("이메일과 비밀번호는 비어 있을 수 없습니다."); + } + + if (loginType.equals(LoginType.DEFAULT) && (email.isBlank() || password.isBlank())) { + throw new BadRequestException("이메일과 비밀번호는 비어 있을 수 없습니다."); + } + } + + private void validateNotNull(String nickname, String profileImageUrl) { + if (nickname == null || profileImageUrl == null) { + throw new BadRequestException("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } } diff --git a/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java b/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java new file mode 100644 index 00000000..97ebaae3 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java @@ -0,0 +1,28 @@ +package kr.touroot.member.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import kr.touroot.member.domain.LoginType; +import kr.touroot.member.domain.Member; + +public record MemberRequest( + @Schema(description = "사용자 이메일", example = "email@gmail.com") + @NotBlank(message = "이메일은 비어있을 수 없습니다.") + @Email + String email, + @Schema(description = "사용자 비밀번호", example = "@testpassword1234") + @NotBlank(message = "비밀번호는 비어있을 수 없습니다.") + String password, + @Schema(description = "사용자 닉네임", example = "뚜리") + @NotBlank(message = "닉네임은 비어있을 수 없습니다.") + String nickname, + @Schema(description = "사용자 프로필 사진 URL", example = "S3 이미지 URL") + @NotBlank(message = "프로필 사진 URL은 비어있을 수 없습니다.") + String profileImageUrl +) { + + public Member toMember(String password) { + return new Member(email, password, nickname, profileImageUrl, LoginType.DEFAULT); + } +} diff --git a/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java b/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java index 4ba5158d..62bfaf2d 100644 --- a/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java +++ b/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java @@ -1,10 +1,16 @@ package kr.touroot.member.repository; import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; import kr.touroot.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { Optional findByKakaoId(Long kakaoId); + + Optional findByEmailAndPassword(String email, String password); + + Optional findByEmail(String email); + + Optional findByNickname(String nickname); } diff --git a/backend/src/main/java/kr/touroot/member/service/MemberService.java b/backend/src/main/java/kr/touroot/member/service/MemberService.java index 5d05fe40..4a3a8802 100644 --- a/backend/src/main/java/kr/touroot/member/service/MemberService.java +++ b/backend/src/main/java/kr/touroot/member/service/MemberService.java @@ -1,7 +1,9 @@ package kr.touroot.member.service; +import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; +import kr.touroot.member.dto.request.MemberRequest; import kr.touroot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,9 +13,35 @@ public class MemberService { private final MemberRepository memberRepository; + private final PasswordEncryptor passwordEncryptor; public Member getById(Long memberId) { return memberRepository.findById(memberId) .orElseThrow(() -> new BadRequestException("존재하지 않는 사용자입니다.")); } + + public Long createMember(MemberRequest request) { + validateRequest(request); + String encryptedPassword = passwordEncryptor.encrypt(request.password()); + Member member = request.toMember(encryptedPassword); + + return memberRepository.save(member).getId(); + } + + private void validateRequest(MemberRequest request) { + validateEmailDuplication(request.email()); + validateNicknameDuplicationr(request.nickname()); + } + + private void validateEmailDuplication(String email) { + if (memberRepository.findByEmail(email).isPresent()) { + throw new BadRequestException("이미 회원 가입되어 있는 이메일입니다."); + } + } + + private void validateNicknameDuplicationr(String nickname) { + if (memberRepository.findByNickname(nickname).isPresent()) { + throw new BadRequestException("이미 사용 중인 닉네임입니다."); + } + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java index 1ad913df..def693d2 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePhotoRequest.java @@ -2,11 +2,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import kr.touroot.travelogue.domain.TraveloguePhoto; -import kr.touroot.travelogue.domain.TraveloguePlace; public record TraveloguePhotoRequest( - @Schema(description = "여행기 장소 사진 URL", example = "photo.png") + @Schema(description = "여행기 장소 사진 URL", example = "S3 이미지 URL") @NotBlank(message = "여행기 장소 사진 URL 값은 비어있을 수 없습니다.") String url ) { diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 0da043d3..7b9b8fcf 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -14,7 +14,7 @@ public record TravelogueRequest( @NotBlank(message = "여행기 제목은 비어있을 수 없습니다.") @Size(message = "여행기 제목은 20자를 초과할 수 없습니다.", max = 20) String title, - @Schema(description = "여행기 썸네일", example = "https://thumbnail.png") + @Schema(description = "여행기 썸네일", example = "S3 이미지 URL") @NotNull(message = "여행기 썸네일은 비어있을 수 없습니다.") String thumbnail, @Schema(description = "여행기 일자 목록") diff --git a/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java index f6e24691..d636f635 100644 --- a/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java +++ b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java @@ -1,8 +1,16 @@ package kr.touroot.authentication.fixture; +import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; +import lombok.AllArgsConstructor; +import lombok.Getter; -public class MemberFixture { +@AllArgsConstructor +@Getter +public enum MemberFixture { - public static final Member MEMBER_1 = new Member(1L, "리비", "http://imageurl.com"); + MEMBER_KAKAO(new Member(1L, "리비", "http://imageurl.com", LoginType.KAKAO)), + MEMBER_DEFAULT(new Member("user@email.com", "5304d46adc6ccffd0", "뚜리", "http://imageurl.com", LoginType.DEFAULT)); + + private final Member member; } diff --git a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java index 0db2ecc4..0ad38ede 100644 --- a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java +++ b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java @@ -1,5 +1,6 @@ package kr.touroot.authentication.service; +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -8,7 +9,6 @@ import java.util.Optional; import kr.touroot.authentication.dto.response.LoginResponse; -import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.authentication.fixture.OauthUserInformationFixture; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.authentication.infrastructure.KakaoOauthProvider; @@ -44,12 +44,12 @@ void existUserKakaoSocialLoginTest() { when(kakaoOauthProvider.getUserInformation(any(String.class), any(String.class))) .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); when(memberRepository.findByKakaoId(any(Long.class))) - .thenReturn(Optional.of(MemberFixture.MEMBER_1)); + .thenReturn(Optional.of(MEMBER_KAKAO.getMember())); LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( - LoginResponse.of(MemberFixture.MEMBER_1, response.accessToken())); + LoginResponse.of(MEMBER_KAKAO.getMember(), response.accessToken())); } @DisplayName("투룻 회원가입이 되어 있지 않은 회원은 소셜 로그인 과정에서 회원가입 후 로그인 된다") @@ -61,12 +61,12 @@ void nonExistUserKakaoSocialLoginTest() { when(memberRepository.findByKakaoId(any(Long.class))) .thenReturn(Optional.empty()); when(memberRepository.save(any(Member.class))) - .thenReturn(MemberFixture.MEMBER_1); + .thenReturn(MEMBER_KAKAO.getMember()); LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( - LoginResponse.of(MemberFixture.MEMBER_1, response.accessToken())); + LoginResponse.of(MEMBER_KAKAO.getMember(), response.accessToken())); verify(memberRepository, times(1)).save(any(Member.class)); } } diff --git a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java index c160a918..f7730917 100644 --- a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java +++ b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java @@ -1,27 +1,15 @@ package kr.touroot.image.infrastructure; -import static io.restassured.RestAssured.when; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import io.restassured.RestAssured; -import kr.touroot.authentication.fixture.OauthUserInformationFixture; -import kr.touroot.global.AcceptanceTest; import kr.touroot.global.exception.BadRequestException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.ContextConfiguration; @@ -67,6 +55,6 @@ void copyImageToPermanentStorageWithInvalidPath() { String imageUrl = "invalid/testUrl.png"; assertThatThrownBy(() -> s3Provider.copyImageToPermanentStorage(imageUrl)) .isInstanceOf(BadRequestException.class) - .hasMessage("이미지 url 형식이 잘못되었습니다."); + .hasMessage("S3 이미지 url 형식이 잘못되었습니다."); } } diff --git a/backend/src/test/java/kr/touroot/member/controller/MemberControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MemberControllerTest.java new file mode 100644 index 00000000..a2f4309c --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/controller/MemberControllerTest.java @@ -0,0 +1,111 @@ +package kr.touroot.member.controller; + +import static kr.touroot.member.fixture.MemberRequestFixture.EMPTY_EMAIL_MEMBER; +import static kr.touroot.member.fixture.MemberRequestFixture.EMPTY_NICKNAME_MEMBER; +import static kr.touroot.member.fixture.MemberRequestFixture.EMPTY_PASSWORD_MEMBER; +import static kr.touroot.member.fixture.MemberRequestFixture.EMPTY_PROFILE_IMAGE_URL_MEMBER; +import static kr.touroot.member.fixture.MemberRequestFixture.VALID_MEMBER; +import static org.hamcrest.Matchers.is; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.member.dto.request.MemberRequest; +import kr.touroot.utils.DatabaseCleaner; +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.web.server.LocalServerPort; + +@DisplayName("사용자 컨트롤러") +@AcceptanceTest +class MemberControllerTest { + + private final DatabaseCleaner databaseCleaner; + + @LocalServerPort + private int port; + + @Autowired + public MemberControllerTest(DatabaseCleaner databaseCleaner) { + this.databaseCleaner = databaseCleaner; + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + + databaseCleaner.executeTruncate(); + } + + @DisplayName("회원 가입을 한다.") + @Test + void createTravelogue() { + MemberRequest request = VALID_MEMBER.getRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/members") + .then().log().all() + .statusCode(201) + .header("Location", "/api/v1/members/1"); + } + + @DisplayName("비어있는 이메일로 회원 가입하면 예외가 발생한다.") + @Test + void createTravelogueWithEmptyEmail() { + MemberRequest request = EMPTY_EMAIL_MEMBER.getRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/members") + .then().log().all() + .statusCode(400) + .body("message", is("이메일은 비어있을 수 없습니다.")); + } + + @DisplayName("비어있는 비밀번호로 회원 가입하면 예외가 발생한다.") + @Test + void createTravelogueWithEmptyPassword() { + MemberRequest request = EMPTY_PASSWORD_MEMBER.getRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/members") + .then().log().all() + .statusCode(400) + .body("message", is("비밀번호는 비어있을 수 없습니다.")); + } + + @DisplayName("비어있는 닉네임으로 회원 가입하면 예외가 발생한다.") + @Test + void createTravelogueWithEmptyNickname() { + MemberRequest request = EMPTY_NICKNAME_MEMBER.getRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/members") + .then().log().all() + .statusCode(400) + .body("message", is("닉네임은 비어있을 수 없습니다.")); + } + + @DisplayName("비어있는 프로필 사진 경로로 회원 가입하면 예외가 발생한다.") + @Test + void createTravelogueWithEmptyProfileImageUrl() { + MemberRequest request = EMPTY_PROFILE_IMAGE_URL_MEMBER.getRequest(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().post("/api/v1/members") + .then().log().all() + .statusCode(400) + .body("message", is("프로필 사진 URL은 비어있을 수 없습니다.")); + } +} diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index b3f5cfec..7beaa31b 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -1,5 +1,7 @@ package kr.touroot.member.controller; +import static org.hamcrest.Matchers.is; + import io.restassured.RestAssured; import io.restassured.http.ContentType; import kr.touroot.authentication.infrastructure.JwtTokenProvider; @@ -15,8 +17,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; -import static org.hamcrest.Matchers.is; - @DisplayName("마이 페이지 컨트롤러") @AcceptanceTest class MyPageControllerTest { @@ -49,7 +49,7 @@ void setUp() { RestAssured.port = port; databaseCleaner.executeTruncate(); - member = travelogueTestHelper.initMemberTestData(); + member = travelogueTestHelper.initKakaoMemberTestData(); accessToken = jwtTokenProvider.createToken(member.getId()); } diff --git a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java index ef7da55a..6fd9c34c 100644 --- a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java +++ b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java @@ -13,45 +13,77 @@ class MemberTest { private static final Long VALID_SOCIAl_ID = 1L; + private static final String VALID_EMAIL = "user@email.com"; private static final String VALID_NICKNAME = "nickname"; - private static final String VALID_PROFILE_IMAGE_URL = "http://touroot.kr/images/1"; + private static final String VALID_PROFILE_IMAGE_URL = "https://dev.touroot.kr/images/ttouri.png"; + private static final LoginType DEFAULT = LoginType.DEFAULT; + private static final LoginType KAKAO = LoginType.KAKAO; + private static final String VALID_PASSWORD = "5304d46adc6ccffd0abf352f3e17ba8b807dc3d5e5d12609d0825d7287"; @DisplayName("검증 규칙을 통과하는 멤버 생성은 예외가 발생하지 않는다") @Test void createMemberWithValidData() { - assertThatCode(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL)) + assertThatCode(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO)) .doesNotThrowAnyException(); } - @DisplayName("카카오 아이디가 null일 때 멤버 생성 시 예외가 발생한다") + @DisplayName("자체 로그인 시, 이메일과 비밀번호가 null일 때 멤버 생성 시 예외가 발생한다") + @Test + void createMemberWithEmailAndPasswordNull() { + assertThatThrownBy(() -> new Member(null, null, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, DEFAULT)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이메일과 비밀번호는 비어 있을 수 없습니다."); + } + + @DisplayName("자체 로그인 시, 이메일이 비어 있는 경우 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void createMemberWithBlankEmail(String blankEmail) { + assertThatThrownBy( + () -> new Member(blankEmail, VALID_PASSWORD, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, DEFAULT)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이메일과 비밀번호는 비어 있을 수 없습니다."); + } + + @DisplayName("프로필 이미지 경로가 비어 있는 경우 멤버 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void createMemberWithBlankPassword(String blankPassword) { + assertThatThrownBy( + () -> new Member(VALID_EMAIL, blankPassword, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, DEFAULT)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이메일과 비밀번호는 비어 있을 수 없습니다."); + } + + @DisplayName("카카오 로그인 시, 카카오 아이디가 null일 때 멤버 생성 시 예외가 발생한다") @Test void createMemberWithKakaoIdNull() { - assertThatThrownBy(() -> new Member(null, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL)) + assertThatThrownBy(() -> new Member(null, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO)) .isInstanceOf(BadRequestException.class) - .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + .hasMessage("카카오 ID는 비어 있을 수 없습니다"); } @DisplayName("닉네임이 null인 경우 멤버 생성 시 예외가 발생한다") @Test void createMemberWithNicknameNull() { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, null, VALID_PROFILE_IMAGE_URL)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, null, VALID_PROFILE_IMAGE_URL, KAKAO)) .isInstanceOf(BadRequestException.class) - .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @DisplayName("프로필 이미지 경로가 null일 경우 멤버 생성 시 예외가 발생한다") @Test void createMemberWithProfileImageUrlNull() { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, null)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, null, KAKAO)) .isInstanceOf(BadRequestException.class) - .hasMessage("카카오 아이디, 닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @DisplayName("닉네임이 비어 있는 경우 멤버 생성 시 예외가 발생한다") @ParameterizedTest @ValueSource(strings = {"", " "}) void createMemberWithBlankNickname(String blankNickname) { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, blankNickname, VALID_PROFILE_IMAGE_URL)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, blankNickname, VALID_PROFILE_IMAGE_URL, KAKAO)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -60,7 +92,7 @@ void createMemberWithBlankNickname(String blankNickname) { @ParameterizedTest @ValueSource(strings = {"", " "}) void createMemberWithProfileImageBlank(String blankUrl) { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, blankUrl)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, blankUrl, KAKAO)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); } @@ -69,7 +101,7 @@ void createMemberWithProfileImageBlank(String blankUrl) { @ParameterizedTest @ValueSource(strings = {"21-length-nicknameeee", "22-length-nicknameeeee"}) void createMemberWithInvalidLengthNickname(String invalidLengthNickname) { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, invalidLengthNickname, VALID_PROFILE_IMAGE_URL)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, invalidLengthNickname, VALID_PROFILE_IMAGE_URL, KAKAO)) .isInstanceOf(BadRequestException.class) .hasMessage("닉네임은 1자 이상, 20자 이하여야 합니다"); } @@ -78,7 +110,7 @@ void createMemberWithInvalidLengthNickname(String invalidLengthNickname) { @ParameterizedTest @ValueSource(strings = {"htt:touroot.kr", "touroot.kr"}) void createMemberWithInvalidProfileImageUrl(String invalidProfileImageUrl) { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, invalidProfileImageUrl)) + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, invalidProfileImageUrl, KAKAO)) .isInstanceOf(BadRequestException.class) .hasMessage("이미지 url 형식이 잘못되었습니다"); } diff --git a/backend/src/test/java/kr/touroot/member/fixture/MemberRequestFixture.java b/backend/src/test/java/kr/touroot/member/fixture/MemberRequestFixture.java new file mode 100644 index 00000000..a62f9105 --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/fixture/MemberRequestFixture.java @@ -0,0 +1,34 @@ +package kr.touroot.member.fixture; + +import kr.touroot.member.dto.request.MemberRequest; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum MemberRequestFixture { + + VALID_MEMBER( + new MemberRequest("user@email.com", "@testpassword", "뚜리", "https://dev.touroot.kr/images/f8c26e9f.png") + ), + EMPTY_EMAIL_MEMBER( + new MemberRequest("", "@testpassword", "뚜리", "https://dev.touroot.kr/images/f8c26e9f.png") + ), + EMPTY_PASSWORD_MEMBER( + new MemberRequest("user@email.com", "", "뚜리", "https://dev.touroot.kr/images/f8c26e9f.png") + ), + EMPTY_NICKNAME_MEMBER( + new MemberRequest("user@email.com", "@testpassword", "", "https://dev.touroot.kr/images/f8c26e9f.png") + ), + EMPTY_PROFILE_IMAGE_URL_MEMBER( + new MemberRequest("user@email.com", "@testpassword", "뚜리", "") + ), + DUPLICATE_EMAIL_MEMBER( + new MemberRequest("user@email.com", "@testpassword", "뚜리뚜리", "https://dev.touroot.kr/images/f8c26e9f.png") + ), + DUPLICATE_NICKNAME_MEMBER( + new MemberRequest("hello@email.com", "@testpassword", "뚜리", "https://dev.touroot.kr/images/f8c26e9f.png") + ); + + private final MemberRequest request; +} diff --git a/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java b/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java new file mode 100644 index 00000000..ebb9ac75 --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java @@ -0,0 +1,25 @@ +package kr.touroot.member.helper; + +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_DEFAULT; + +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MemberTestHelper { + + private final MemberRepository memberRepository; + + @Autowired + public MemberTestHelper(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public Member persistMember() { + Member member = MEMBER_DEFAULT.getMember(); + + return memberRepository.save(member); + } +} diff --git a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java new file mode 100644 index 00000000..0347129b --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java @@ -0,0 +1,94 @@ +package kr.touroot.member.service; + +import static kr.touroot.member.fixture.MemberRequestFixture.DUPLICATE_EMAIL_MEMBER; +import static kr.touroot.member.fixture.MemberRequestFixture.DUPLICATE_NICKNAME_MEMBER; +import static kr.touroot.member.fixture.MemberRequestFixture.VALID_MEMBER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.authentication.infrastructure.PasswordEncryptor; +import kr.touroot.global.ServiceTest; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.Member; +import kr.touroot.member.dto.request.MemberRequest; +import kr.touroot.member.helper.MemberTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.context.annotation.Import; + +@DisplayName("사용자 서비스") +@Import(value = {MemberService.class, MemberTestHelper.class, PasswordEncryptor.class}) +@ServiceTest +class MemberServiceTest { + + private final MemberService memberService; + private final MemberTestHelper testHelper; + private final DatabaseCleaner databaseCleaner; + + @Autowired + public MemberServiceTest( + MemberService memberService, + MemberTestHelper testHelper, + DatabaseCleaner databaseCleaner + ) { + this.memberService = memberService; + this.testHelper = testHelper; + this.databaseCleaner = databaseCleaner; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @DisplayName("ID를 기준으로 회원을 조회한다.") + @Test + void getById() { + Member member = testHelper.persistMember(); + + assertThat(memberService.getById(member.getId()).getId()) + .isEqualTo(member.getId()); + } + + @DisplayName("ID를 기준으로 존재하지 않는 회원을 조회하면 예외가 발생한다.") + @Test + void getByIdNotExist() { + assertThatThrownBy(() -> memberService.getById(0L)) + .hasMessage("존재하지 않는 사용자입니다."); + } + + @DisplayName("정상적인 값을 가진 요청이 주어지면 회원을 생성한다.") + @Test + void createMember() { + MemberRequest request = VALID_MEMBER.getRequest(); + + Long id = memberService.createMember(request); + + assertThat(id).isEqualTo(1L); + } + + @DisplayName("중복된 이메일을 가진 회원을 생성하려하면 예외가 발생한다.") + @Test + void createMemberWithDuplicatedEmail() { + testHelper.persistMember(); + MemberRequest request = DUPLICATE_EMAIL_MEMBER.getRequest(); + + assertThatThrownBy(() -> memberService.createMember(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 회원 가입되어 있는 이메일입니다."); + } + + @DisplayName("중복된 이메일을 가진 회원을 생성하려하면 예외가 발생한다.") + @Test + void createMemberWithDuplicatedNickname() { + testHelper.persistMember(); + MemberRequest request = DUPLICATE_NICKNAME_MEMBER.getRequest(); + + assertThatThrownBy(() -> memberService.createMember(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 사용 중인 닉네임입니다."); + } +} \ No newline at end of file diff --git a/backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java b/backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java index b0b46f17..a125a994 100644 --- a/backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java +++ b/backend/src/test/java/kr/touroot/place/fixture/PlaceFixture.java @@ -1,7 +1,9 @@ package kr.touroot.place.fixture; import kr.touroot.place.domain.Place; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum PlaceFixture { PLACE("함덕해수욕장", "34.54343", "126.66977", ""); @@ -11,13 +13,6 @@ public enum PlaceFixture { private final String longitude; private final String googlePlaceId; - PlaceFixture(String name, String latitude, String longitude, String googlePlaceId) { - this.name = name; - this.latitude = latitude; - this.longitude = longitude; - this.googlePlaceId = googlePlaceId; - } - public Place get() { return new Place(name, latitude, longitude, googlePlaceId); } diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 0b207043..9ea1eb04 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -70,7 +70,7 @@ void createTravelogue() { .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); - Member member = testHelper.initMemberTestData(); + Member member = testHelper.initKakaoMemberTestData(); String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() @@ -138,7 +138,7 @@ void findNotExistTravelogueThrowException() { @DisplayName("여행기를 삭제한다.") @Test void deleteTravelogue() { - Member member = testHelper.initMemberTestData(); + Member member = testHelper.initKakaoMemberTestData(); testHelper.initTravelogueTestData(member); String accessToken = jwtTokenProvider.createToken(member.getId()); @@ -152,7 +152,7 @@ void deleteTravelogue() { @DisplayName("존재하지 않는 여행기 삭제시 400를 응답한다.") @Test void deleteTravelogueWithNonExist() { - Member member = testHelper.initMemberTestData(); + Member member = testHelper.initKakaoMemberTestData(); String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() @@ -167,7 +167,7 @@ void deleteTravelogueWithNonExist() { @Test void deleteTravelogueWithNotAuthor() { Travelogue travelogue = testHelper.initTravelogueTestData(); - Member notAuthor = testHelper.initMemberTestData(); + Member notAuthor = testHelper.initKakaoMemberTestData(); String accessToken = jwtTokenProvider.createToken(notAuthor.getId()); RestAssured.given().log().all() diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java index 50139a66..946bf8d3 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueDayTest.java @@ -1,10 +1,10 @@ package kr.touroot.travelogue.domain; +import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import kr.touroot.global.exception.BadRequestException; -import kr.touroot.travelogue.fixture.TravelogueFixture; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,7 +14,7 @@ class TravelogueDayTest { private static final Integer VALID_ORDER = 0; - private static final Travelogue VALID_TRAVELOGUE = TravelogueFixture.TRAVELOGUE.get(); + private static final Travelogue VALID_TRAVELOGUE = TRAVELOGUE.get(); @DisplayName("유효한 여행 날짜 생성 시 예외가 발생하지 않는다") @Test diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java index 73ff341f..8212fd0d 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java @@ -1,9 +1,9 @@ package kr.touroot.travelogue.domain; +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import org.junit.jupiter.api.DisplayName; @@ -14,7 +14,7 @@ @DisplayName("여행기 도메인") class TravelogueTest { - private static final Member VALID_AUTHOR = MemberFixture.MEMBER_1; + private static final Member VALID_AUTHOR = MEMBER_KAKAO.getMember(); private static final String VALID_TITLE = "올바른 여행기 제목"; private static final String VALID_THUMBNAIL = "http://valid-thumbnail.com"; diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java index 768d15ef..116ebcc3 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueDayFixture.java @@ -4,7 +4,9 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum TravelogueDayFixture { TRAVELOGUE_DAY(1, TRAVELOGUE.get()); @@ -12,11 +14,6 @@ public enum TravelogueDayFixture { private final int order; private final Travelogue travelogue; - TravelogueDayFixture(int order, Travelogue travelogue) { - this.order = order; - this.travelogue = travelogue; - } - public TravelogueDay get() { return new TravelogueDay(order, travelogue); } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java index 979381ab..b36c1918 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java @@ -1,23 +1,20 @@ package kr.touroot.travelogue.fixture; -import kr.touroot.authentication.fixture.MemberFixture; +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; + import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum TravelogueFixture { - TRAVELOGUE(MemberFixture.MEMBER_1, "제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png"); + TRAVELOGUE(MEMBER_KAKAO.getMember(), "제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png"); private final Member author; private final String title; private final String thumbnail; - TravelogueFixture(Member author, String title, String thumbnail) { - this.author = author; - this.title = title; - this.thumbnail = thumbnail; - } - public Travelogue get() { return new Travelogue(author, title, thumbnail); } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java index 64c019db..c587cc11 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePhotoFixture.java @@ -4,7 +4,9 @@ import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum TraveloguePhotoFixture { TRAVELOGUE_PHOTO(1, "https://dev.touroot.kr/temporary/image1.png", TRAVELOGUE_PLACE.get()); @@ -13,12 +15,6 @@ public enum TraveloguePhotoFixture { private final String url; private final TraveloguePlace place; - TraveloguePhotoFixture(int order, String url, TraveloguePlace place) { - this.order = order; - this.url = url; - this.place = place; - } - public TraveloguePhoto get() { return new TraveloguePhoto(order, url, place); } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java index 23d28a88..428632e8 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TraveloguePlaceFixture.java @@ -6,7 +6,9 @@ import kr.touroot.place.domain.Place; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePlace; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum TraveloguePlaceFixture { TRAVELOGUE_PLACE(1, "에메랄드 빛 해변", PLACE.get(), TRAVELOGUE_DAY.get()); @@ -16,13 +18,6 @@ public enum TraveloguePlaceFixture { private final Place place; private final TravelogueDay day; - TraveloguePlaceFixture(int order, String description, Place place, TravelogueDay day) { - this.order = order; - this.description = description; - this.place = place; - this.day = day; - } - public TraveloguePlace get() { return new TraveloguePlace(order, description, place, day); } diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 38a8f1ac..3a508ece 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,6 +1,13 @@ package kr.touroot.travelogue.helper; -import kr.touroot.authentication.fixture.MemberFixture; +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; +import static kr.touroot.place.fixture.PlaceFixture.PLACE; +import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; +import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; +import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; +import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; + +import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; @@ -16,12 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import static kr.touroot.place.fixture.PlaceFixture.PLACE; -import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; -import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; -import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; -import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; - @Component public class TravelogueTestHelper { @@ -73,7 +74,7 @@ public void initTravelogueTestDate(Member author) { } public Member persistMember() { - Member author = MemberFixture.MEMBER_1; + Member author = MEMBER_KAKAO.getMember(); return memberRepository.save(author); } @@ -108,8 +109,8 @@ public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { return traveloguePhotoRepository.save(photo); } - public Member initMemberTestData() { - Member member = new Member(1L, "tester", "http://image.com"); + public Member initKakaoMemberTestData() { + Member member = new Member(1L, "tester", "http://image.com", LoginType.KAKAO); return memberRepository.save(member); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 9dc4d3f1..aff72709 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; +import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; @@ -35,6 +36,7 @@ MemberService.class, TravelogueTestHelper.class, AwsS3Provider.class, + PasswordEncryptor.class }) @ServiceTest class TravelogueFacadeServiceTest { @@ -73,7 +75,7 @@ void createTravelogue() { TravelogueRequestFixture.getTraveloguePhotoRequests().get(0).url()) ).thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); - testHelper.initMemberTestData(); + testHelper.initKakaoMemberTestData(); MemberAuth memberAuth = new MemberAuth(1L); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); @@ -116,7 +118,7 @@ void deleteById() { @DisplayName("존재하지 않는 ID로 여행기를 삭제하면 예외가 발생한다.") @Test void deleteTravelogueByNotExistsIdThrowException() { - MemberAuth memberAuth = new MemberAuth(testHelper.initMemberTestData().getId()); + MemberAuth memberAuth = new MemberAuth(testHelper.initKakaoMemberTestData().getId()); assertThatThrownBy(() -> service.deleteTravelogueById(1L, memberAuth)) .isInstanceOf(BadRequestException.class) @@ -127,7 +129,7 @@ void deleteTravelogueByNotExistsIdThrowException() { @Test void deleteByIdWithNotAuthor() { testHelper.initTravelogueTestData(); - MemberAuth notAuthorAuth = new MemberAuth(testHelper.initMemberTestData().getId()); + MemberAuth notAuthorAuth = new MemberAuth(testHelper.initKakaoMemberTestData().getId()); assertThatThrownBy(() -> service.deleteTravelogueById(1L, notAuthorAuth)) .isInstanceOf(ForbiddenException.class) diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 4134bde1..cff180c9 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -63,7 +63,7 @@ void createTravelogue() { Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); - Member author = testHelper.initMemberTestData(); + Member author = testHelper.initKakaoMemberTestData(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); Travelogue createdTravelogue = travelogueService.createTravelogue(author, request); @@ -102,7 +102,7 @@ void findAll() { @DisplayName("여행기를 삭제할 수 있다.") @Test void deleteTravelogueById() { - Member author = testHelper.initMemberTestData(); + Member author = testHelper.initKakaoMemberTestData(); Travelogue travelogue = testHelper.initTravelogueTestData(author); long travelogueId = travelogue.getId(); @@ -117,7 +117,7 @@ void deleteTravelogueById() { @Test void deleteTravelogueByNotAuthorThrowException() { Travelogue travelogue = testHelper.initTravelogueTestData(); - Member notAuthor = testHelper.initMemberTestData(); + Member notAuthor = testHelper.initKakaoMemberTestData(); assertThatThrownBy(() -> travelogueService.delete(travelogue, notAuthor)) .isInstanceOf(ForbiddenException.class) diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index e3d1c609..529728bc 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -1,13 +1,14 @@ package kr.touroot.travelplan.domain; +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; import java.util.UUID; -import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.global.exception.BadRequestException; +import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,7 +18,7 @@ @DisplayName("여행 계획") class TravelPlanTest { - private static final Member VALID_AUTHOR = MemberFixture.MEMBER_1; + private static final Member VALID_AUTHOR = MEMBER_KAKAO.getMember(); private static final String VALID_TITLE = "제주도 여행 계획"; private static final UUID VALID_UUID = UUID.randomUUID(); private static final LocalDate VALID_START_DATE = LocalDate.now().plusDays(2); @@ -92,9 +93,9 @@ void validateStartDate() { @Test void validateAuthor() { // given - Member author = new Member(1L, 1L, "tester", "http://url.com"); + Member author = new Member(1L, 1L, null, null, "tester", "http://url.com", LoginType.KAKAO); TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN, VALID_UUID, author); - Member notAuthor = new Member(2L, 2L, "tester2", "http://url.com"); + Member notAuthor = new Member(2L, 2L, null, null, "tester2", "http://url.com", LoginType.KAKAO); // when boolean actual = travelPlan.isAuthor(notAuthor); diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java index 1d9327c1..c5dcefb9 100644 --- a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanDayFixture.java @@ -4,7 +4,9 @@ import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.domain.TravelPlanDay; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum TravelPlanDayFixture { TRAVEL_PLAN_DAY(0, TRAVEL_PLAN.get()); @@ -12,11 +14,6 @@ public enum TravelPlanDayFixture { private final int order; private final TravelPlan travelPlan; - TravelPlanDayFixture(int order, TravelPlan travelPlan) { - this.order = order; - this.travelPlan = travelPlan; - } - public TravelPlanDay get() { return new TravelPlanDay(order, travelPlan); } diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java index 770c36e4..c1952717 100644 --- a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java @@ -1,25 +1,22 @@ package kr.touroot.travelplan.fixture; +import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; + import java.time.LocalDate; import java.util.UUID; -import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; +import lombok.AllArgsConstructor; +@AllArgsConstructor public enum TravelPlanFixture { - TRAVEL_PLAN("제주도 여행 계획", LocalDate.now().plusDays(2), MemberFixture.MEMBER_1); + TRAVEL_PLAN("제주도 여행 계획", LocalDate.now().plusDays(2), MEMBER_KAKAO.getMember()); private final String title; private final LocalDate startDate; private final Member author; - TravelPlanFixture(String title, LocalDate startDate, Member author) { - this.title = title; - this.startDate = startDate; - this.author = author; - } - public TravelPlan get() { return new TravelPlan(title, startDate, UUID.randomUUID(), author); } diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 59412561..4795a186 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -1,5 +1,8 @@ package kr.touroot.travelplan.helper; +import java.time.LocalDate; +import java.util.UUID; +import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; @@ -13,9 +16,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.time.LocalDate; -import java.util.UUID; - @Component public class TravelPlanTestHelper { @@ -40,8 +40,8 @@ public TravelPlanTestHelper( this.memberRepository = memberRepository; } - public static Member getMember(Long kakaoId, String nickname, String profileImageUri) { - return new Member(kakaoId, nickname, profileImageUri); + public static Member getKakaoMember(Long kakaoId, String nickname, String profileImageUri) { + return new Member(kakaoId, nickname, profileImageUri, LoginType.KAKAO); } public static Place getPlace(String name, String latitude, String longitude, String googlePlaceId) { @@ -90,7 +90,7 @@ public TravelPlan initTravelPlanTestData(Member author) { } public Member initMemberTestData() { - Member member = getMember(1L, "tester", "http://image.com"); + Member member = getKakaoMember(1L, "tester", "http://image.com"); return memberRepository.save(member); } } From c01441cea75e2fd97cc8155cfb01203e2814ee21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:46:31 +0900 Subject: [PATCH 078/108] =?UTF-8?q?[Fix]=20-=20=EC=82=AD=EC=A0=9C=20API=20?= =?UTF-8?q?CORS=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20#239?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/kr/touroot/global/config/WebConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/global/config/WebConfig.java b/backend/src/main/java/kr/touroot/global/config/WebConfig.java index 25fe59d5..2086cc05 100644 --- a/backend/src/main/java/kr/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebConfig.java @@ -22,6 +22,7 @@ public void addArgumentResolvers(List resolvers) @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr"); + .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr") + .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS"); } } From 134dda55857217b76e7189c74fae603b2781db07 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:12:58 +0900 Subject: [PATCH 079/108] =?UTF-8?q?[Fix]=20-=20JwtAuthFilter=20Authorizati?= =?UTF-8?q?on=20Header=20=ED=8C=8C=EC=8B=B1=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index e3c20aac..ddc91fb4 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -47,7 +47,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader(HttpHeaders.AUTHORIZATION); - if (token == null) { + if (token == null || token.isBlank()) { sendUnauthorizedResponse(response, "로그인을 해주세요."); return; } From 42aea59f60c7ab6a245fe3b3f3fde7f1d0cd9378 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:25:29 +0900 Subject: [PATCH 080/108] =?UTF-8?q?[Feature]=20-=20=ED=83=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Tag Entity 추가 * feat: 태그 생성 구현 * docs: 태그 생성 Swagger 작성 * feat: 태그 조회 API * fix: 태그 컨트롤러 url 수정 * feat: 태그 조회 API 캐싱 추가 * docs: 태그 생성 ApiResponse Operation description 수정 * feat: Tag Entity tag에 unqiue 제약 조건 * fix: createTag savedTag 오타 수정 --- backend/build.gradle | 4 ++ .../java/kr/touroot/TourootApplication.java | 2 + .../kr/touroot/global/auth/JwtAuthFilter.java | 2 + .../java/kr/touroot/tag/TagController.java | 63 +++++++++++++++++++ .../kr/touroot/tag/cache/CacheConfig.java | 25 ++++++++ .../main/java/kr/touroot/tag/domain/Tag.java | 25 ++++++++ .../kr/touroot/tag/dto/TagCreateRequest.java | 15 +++++ .../java/kr/touroot/tag/dto/TagResponse.java | 19 ++++++ .../touroot/tag/repository/TagRepository.java | 9 +++ .../kr/touroot/tag/service/TagService.java | 43 +++++++++++++ .../kr/touroot/tag/TagControllerTest.java | 56 +++++++++++++++++ .../kr/touroot/tag/fixture/TagFixture.java | 24 +++++++ .../kr/touroot/tag/helper/TagTestHelper.java | 27 ++++++++ .../touroot/tag/service/TagServiceTest.java | 52 +++++++++++++++ 14 files changed, 366 insertions(+) create mode 100644 backend/src/main/java/kr/touroot/tag/TagController.java create mode 100644 backend/src/main/java/kr/touroot/tag/cache/CacheConfig.java create mode 100644 backend/src/main/java/kr/touroot/tag/domain/Tag.java create mode 100644 backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java create mode 100644 backend/src/main/java/kr/touroot/tag/dto/TagResponse.java create mode 100644 backend/src/main/java/kr/touroot/tag/repository/TagRepository.java create mode 100644 backend/src/main/java/kr/touroot/tag/service/TagService.java create mode 100644 backend/src/test/java/kr/touroot/tag/TagControllerTest.java create mode 100644 backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java create mode 100644 backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java create mode 100644 backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java diff --git a/backend/build.gradle b/backend/build.gradle index 847fe90c..94668462 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -47,6 +47,10 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // cache + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") } tasks.named('test') { diff --git a/backend/src/main/java/kr/touroot/TourootApplication.java b/backend/src/main/java/kr/touroot/TourootApplication.java index 3eb9029d..8cc4e6ee 100644 --- a/backend/src/main/java/kr/touroot/TourootApplication.java +++ b/backend/src/main/java/kr/touroot/TourootApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableCaching @EnableJpaAuditing @SpringBootApplication public class TourootApplication { diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index ddc91fb4..f47c1f54 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -39,6 +39,8 @@ public class JwtAuthFilter extends OncePerRequestFilter { new HttpRequestInfo(HttpMethod.GET, "/api/v1/travelogues/**"), new HttpRequestInfo(HttpMethod.POST, "/api/v1/login/**"), new HttpRequestInfo(HttpMethod.GET, "/api/v1/travel-plans/shared/**"), + new HttpRequestInfo(HttpMethod.POST, "/api/v1/tags/**"), + new HttpRequestInfo(HttpMethod.GET, "/api/v1/tags/**"), new HttpRequestInfo(HttpMethod.POST, "/api/v1/members"), new HttpRequestInfo(HttpMethod.OPTIONS, "/**") ); diff --git a/backend/src/main/java/kr/touroot/tag/TagController.java b/backend/src/main/java/kr/touroot/tag/TagController.java new file mode 100644 index 00000000..a2d925f3 --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/TagController.java @@ -0,0 +1,63 @@ +package kr.touroot.tag; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.net.URI; +import java.util.List; +import kr.touroot.global.exception.dto.ExceptionResponse; +import kr.touroot.tag.dto.TagCreateRequest; +import kr.touroot.tag.dto.TagResponse; +import kr.touroot.tag.service.TagService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "태그") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/tags") +public class TagController { + + private final TagService tagService; + + @Operation(summary = "태그 생성") + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "태그가 생성이 정상적으로 성공했을 때" + ), + @ApiResponse( + responseCode = "400", + description = "Body에 유효하지 않은 값이 존재하거나 중복된 태그가 존재할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PostMapping + public ResponseEntity createTag(@Valid @RequestBody TagCreateRequest request) { + TagResponse data = tagService.createTag(request); + return ResponseEntity.created(URI.create("/api/v1/tags/" + data.id())) + .body(data); + } + + @Operation(summary = "모든 태그 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "태그가 조회가 정상적으로 성공했을 때" + ) + }) + @GetMapping + public ResponseEntity> readTags() { + List data = tagService.readTags(); + return ResponseEntity.ok(data); + } +} diff --git a/backend/src/main/java/kr/touroot/tag/cache/CacheConfig.java b/backend/src/main/java/kr/touroot/tag/cache/CacheConfig.java new file mode 100644 index 00000000..3fe25432 --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/cache/CacheConfig.java @@ -0,0 +1,25 @@ +package kr.touroot.tag.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Duration; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager("tag"); + cacheManager.setCaffeine(caffeineConfig()); + + return cacheManager; + } + + private Caffeine caffeineConfig() { + return Caffeine.newBuilder() + .expireAfterWrite(Duration.ofDays(1)); + } +} diff --git a/backend/src/main/java/kr/touroot/tag/domain/Tag.java b/backend/src/main/java/kr/touroot/tag/domain/Tag.java new file mode 100644 index 00000000..58bb4fb0 --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/domain/Tag.java @@ -0,0 +1,25 @@ +package kr.touroot.tag.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class Tag { + + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @Column(nullable = false, unique = true) + private String tag; + + public Tag(String tag) { + this(null, tag); + } +} diff --git a/backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java b/backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java new file mode 100644 index 00000000..5c97f88f --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/dto/TagCreateRequest.java @@ -0,0 +1,15 @@ +package kr.touroot.tag.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import kr.touroot.tag.domain.Tag; + +public record TagCreateRequest( + @Schema(description = "태그 이름", example = "강아지와 함께") + @NotEmpty(message = "태그는 비어있을 수 없습니다.") String tag +) { + + public Tag toTag() { + return new Tag(tag); + } +} diff --git a/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java b/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java new file mode 100644 index 00000000..c0fde1b5 --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java @@ -0,0 +1,19 @@ +package kr.touroot.tag.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.touroot.tag.domain.Tag; +import lombok.Builder; + +@Builder +public record TagResponse( + @Schema(description = "생성된 태그의 id") Long id, + @Schema(description = "생성된 태그의 내용") String tag +) { + + public static TagResponse from(Tag tag) { + return TagResponse.builder() + .id(tag.getId()) + .tag(tag.getTag()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java b/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java new file mode 100644 index 00000000..23430a27 --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/repository/TagRepository.java @@ -0,0 +1,9 @@ +package kr.touroot.tag.repository; + +import kr.touroot.tag.domain.Tag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TagRepository extends JpaRepository { + + boolean existsByTag(String tag); +} diff --git a/backend/src/main/java/kr/touroot/tag/service/TagService.java b/backend/src/main/java/kr/touroot/tag/service/TagService.java new file mode 100644 index 00000000..645dd53c --- /dev/null +++ b/backend/src/main/java/kr/touroot/tag/service/TagService.java @@ -0,0 +1,43 @@ +package kr.touroot.tag.service; + +import java.util.List; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.tag.domain.Tag; +import kr.touroot.tag.dto.TagCreateRequest; +import kr.touroot.tag.dto.TagResponse; +import kr.touroot.tag.repository.TagRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Service +public class TagService { + + private final TagRepository tagRepository; + + @Transactional + public TagResponse createTag(TagCreateRequest tagCreateRequest) { + validateDuplicated(tagCreateRequest); + Tag savedTag = tagRepository.save(tagCreateRequest.toTag()); + + return TagResponse.from(savedTag); + } + + private void validateDuplicated(TagCreateRequest tagCreateRequest) { + if (tagRepository.existsByTag(tagCreateRequest.tag())) { + throw new BadRequestException("이미 존재하는 태그입니다."); + } + } + + @Cacheable(cacheNames = "tag") + @Transactional(readOnly = true) + public List readTags() { + return tagRepository.findAll().stream() + .map(TagResponse::from) + .toList(); + } +} diff --git a/backend/src/test/java/kr/touroot/tag/TagControllerTest.java b/backend/src/test/java/kr/touroot/tag/TagControllerTest.java new file mode 100644 index 00000000..ba251054 --- /dev/null +++ b/backend/src/test/java/kr/touroot/tag/TagControllerTest.java @@ -0,0 +1,56 @@ +package kr.touroot.tag; + +import static org.hamcrest.Matchers.is; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.tag.dto.TagCreateRequest; +import kr.touroot.tag.fixture.TagFixture; +import kr.touroot.tag.helper.TagTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.web.server.LocalServerPort; + +@DisplayName("태그 컨트롤러") +@AcceptanceTest +class TagControllerTest { + + private final DatabaseCleaner databaseCleaner; + private final TagTestHelper testHelper; + + @LocalServerPort + private int port; + + @Autowired + public TagControllerTest(DatabaseCleaner databaseCleaner, TagTestHelper testHelper) { + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + databaseCleaner.executeTruncate(); + } + + @DisplayName("태그 컨트롤러는 태그 생성 요청 시 201을 응답한다.") + @Test + void createTag() { + // given + TagCreateRequest request = TagFixture.TAG.getCreateRequest(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(request) + .when().log().all() + .post("/api/v1/tags") + .then().log().all() + .statusCode(201) + .header("Location", is("/api/v1/tags/1")); + } +} diff --git a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java new file mode 100644 index 00000000..c2733ea6 --- /dev/null +++ b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java @@ -0,0 +1,24 @@ +package kr.touroot.tag.fixture; + +import kr.touroot.tag.domain.Tag; +import kr.touroot.tag.dto.TagCreateRequest; + +public enum TagFixture { + + TAG("강아지와 함께"), + ; + + private final String tag; + + TagFixture(String tag) { + this.tag = tag; + } + + public Tag get() { + return new Tag(tag); + } + + public TagCreateRequest getCreateRequest() { + return new TagCreateRequest(tag); + } +} diff --git a/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java b/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java new file mode 100644 index 00000000..2d6a26e0 --- /dev/null +++ b/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java @@ -0,0 +1,27 @@ +package kr.touroot.tag.helper; + +import kr.touroot.tag.domain.Tag; +import kr.touroot.tag.fixture.TagFixture; +import kr.touroot.tag.repository.TagRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TagTestHelper { + + private final TagRepository tagRepository; + + @Autowired + public TagTestHelper(TagRepository tagRepository) { + this.tagRepository = tagRepository; + } + + public Tag initTagData() { + Tag tag = TagFixture.TAG.get();; + return tagRepository.save(tag); + } + + public Tag initTagData(Tag tag) { + return tagRepository.save(tag); + } +} diff --git a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java new file mode 100644 index 00000000..19ce1761 --- /dev/null +++ b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java @@ -0,0 +1,52 @@ +package kr.touroot.tag.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.ServiceTest; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.tag.domain.Tag; +import kr.touroot.tag.dto.TagCreateRequest; +import kr.touroot.tag.fixture.TagFixture; +import kr.touroot.tag.helper.TagTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.context.annotation.Import; + +@DisplayName("태그 서비스") +@Import(value = {TagService.class, TagTestHelper.class}) +@ServiceTest +class TagServiceTest { + + private final DatabaseCleaner databaseCleaner; + private final TagTestHelper testHelper; + private final TagService tagService; + + @Autowired + public TagServiceTest(DatabaseCleaner databaseCleaner, TagTestHelper testHelper, TagService tagService) { + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + this.tagService = tagService; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @DisplayName("태그 서비스는 중복된 태그 생성 요청시 예외가 발생한다.") + @Test + void validateDuplicated() { + // given + Tag tag = TagFixture.TAG.get(); + testHelper.initTagData(tag); + TagCreateRequest request = new TagCreateRequest(tag.getTag()); + + // when & then + assertThatThrownBy(() -> tagService.createTag(request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 존재하는 태그입니다."); + } +} From 96c30bf318ee0f22a11c977a822b5a399f922722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Wed, 7 Aug 2024 15:29:13 +0900 Subject: [PATCH 081/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D,=20=EC=97=AC=ED=96=89=EA=B8=B0=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EB=B3=80=EA=B2=BD=20(#244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 하위 도메인 조회 시 Id 속성 추가하도록 수정 * feat: 여행 계획 하위 도메인 조회 시 Id 속성 추가하도록 수정 * fix: 오늘 날짜의 여행을 계획하지 못하는 오류 해결 * feat: 여행기 조회시 작성일자 속성 추가 반환 * feat: 여행기 조회 시 작성자를 확인할 수 있도록 응답 필드 추가 * fix: api 명세 오타 수정 --- .../touroot/travelogue/domain/Travelogue.java | 4 ++++ .../response/TraveloguePositionResponse.java | 3 +++ .../dto/response/TravelogueResponse.java | 14 +++++------ .../touroot/travelplan/domain/TravelPlan.java | 10 ++------ .../dto/response/TravelPlanDayResponse.java | 7 +++--- .../dto/response/TravelPlanPlaceResponse.java | 2 ++ .../response/TravelPlanPositionResponse.java | 2 ++ .../travelplan/service/TravelPlanService.java | 7 +++--- .../fixture/TravelogueResponseFixture.java | 6 +++++ .../helper/TravelogueTestHelper.java | 2 +- .../travelplan/domain/TravelPlanTest.java | 24 +++++++++++-------- .../service/TravelPlanServiceTest.java | 23 ++++++++++++++++++ 12 files changed, 72 insertions(+), 32 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index f60f4e6f..a193891a 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -93,4 +93,8 @@ private void validateThumbnailFormat(String thumbnailUrl) { public boolean isAuthor(Member author) { return Objects.equals(author.getId(), this.author.getId()); } + + public String getAuthorNickname() { + return author.getNickname(); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java index 36051242..3f7f3112 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TraveloguePositionResponse.java @@ -6,6 +6,8 @@ @Builder public record TraveloguePositionResponse( + @Schema(description = "여행기 장소 상세 ID", example = "1") + Long id, @Schema(description = "여행기 장소 위도", example = "37.5175896") String lat, @Schema(description = "여행기 장소 설명", example = "127.0867236") @@ -14,6 +16,7 @@ public record TraveloguePositionResponse( public static TraveloguePositionResponse from(TraveloguePlace place) { return TraveloguePositionResponse.builder() + .id(place.getPlace().getId()) .lat(place.getLatitude()) .lng(place.getLongitude()) .build(); diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index 47d06d8f..0438d1fd 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -1,8 +1,7 @@ package kr.touroot.travelogue.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; import java.util.List; import kr.touroot.travelogue.domain.Travelogue; import lombok.Builder; @@ -10,23 +9,24 @@ @Builder public record TravelogueResponse( @Schema(description = "여행기 ID", example = "1") - @NotNull(message = "ID는 비어있을 수 없습니다.") Long id, @Schema(description = "여행기 제목", example = "서울 강남 여행기") - @NotNull(message = "여행기 제목은 비어있을 수 없습니다.") String title, + @Schema(description = "작성자 닉네임", example = "지니") + String authorNickname, @Schema(description = "여행기 썸네일 링크", example = "https://썸네일.png") - @NotNull(message = "여행기 썸네일 링크는 비어있을 수 없습니다.") String thumbnail, + @Schema(description = "작성 날짜") + LocalDate createdAt, @Schema(description = "여행기 일자 목록") - @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") - @Valid List days ) { public static TravelogueResponse of(Travelogue travelogue, List days) { return TravelogueResponse.builder() .id(travelogue.getId()) + .createdAt(travelogue.getCreatedAt().toLocalDate()) + .authorNickname(travelogue.getAuthorNickname()) .title(travelogue.getTitle()) .thumbnail(travelogue.getThumbnail()) .days(days) diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 86cdfcbb..42193f65 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -83,14 +83,8 @@ private void validateTitleLength(String title) { } } - public void validateStartDate() { - if (startDate.isBefore(LocalDate.now())) { - throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); - } - } - - public boolean isValidStartDate() { - return startDate.isAfter(LocalDate.now()); + public boolean isStartDateBefore(LocalDate date) { + return startDate.isBefore(date); } public boolean isAuthor(Member member) { diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java index e1e1acd3..c3044dcd 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java @@ -1,14 +1,14 @@ package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; -import kr.touroot.travelplan.domain.TravelPlanDay; - import java.time.LocalDate; import java.util.List; +import kr.touroot.travelplan.domain.TravelPlanDay; +import lombok.Builder; @Builder public record TravelPlanDayResponse( + @Schema(description = "여행 날짜 Id") Long id, @Schema(description = "여행 일자") LocalDate date, @Schema(description = "여행 장소별 정보") List places ) { @@ -18,6 +18,7 @@ public static TravelPlanDayResponse of( List places ) { return TravelPlanDayResponse.builder() + .id(planDay.getId()) .date(planDay.getCurrentDate()) .places(places) .build(); diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java index 27c7bf1b..ac037f22 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java @@ -7,6 +7,7 @@ @Builder public record TravelPlanPlaceResponse( + @Schema(description = "여행 장소 Id", example = "1") Long id, @Schema(description = "여행 장소 이름", example = "잠실한강공원") String placeName, @Schema(description = "여행 장소 위치") TravelPlanPositionResponse position, @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") String description @@ -17,6 +18,7 @@ public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) { TravelPlanPositionResponse locationResponse = TravelPlanPositionResponse.from(place); return TravelPlanPlaceResponse.builder() + .id(planPlace.getId()) .placeName(place.getName()) .position(locationResponse) .description(planPlace.getDescription()) diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java index 75bc788e..39721f30 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java @@ -6,12 +6,14 @@ @Builder public record TravelPlanPositionResponse( + @Schema(description = "여행 획 장소 상세 Id", example = "1") Long id, @Schema(description = "여행 장소 위도", example = "37.5175896") String lat, @Schema(description = "여행 계획 경도", example = "127.0867236") String lng ) { public static TravelPlanPositionResponse from(Place place) { return TravelPlanPositionResponse.builder() + .id(place.getId()) .lat(place.getLatitude()) .lng(place.getLongitude()) .build(); diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index 640dc8c5..c75c6605 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -1,5 +1,6 @@ package kr.touroot.travelplan.service; +import java.time.LocalDate; import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -43,7 +44,7 @@ public class TravelPlanService { public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request, MemberAuth memberAuth) { Member author = getMemberByMemberAuth(memberAuth); TravelPlan travelPlan = request.toTravelPlan(author, UUID.randomUUID()); - validStartDate(travelPlan); + validateTravelPlan(travelPlan); TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); createPlanDay(request.days(), savedTravelPlan); @@ -51,8 +52,8 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request return new TravelPlanCreateResponse(savedTravelPlan.getId()); } - private void validStartDate(TravelPlan travelPlan) { - if (!travelPlan.isValidStartDate()) { + private void validateTravelPlan(TravelPlan travelPlan) { + if (travelPlan.isStartDateBefore(LocalDate.now())) { throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 3b4c54ac..d9de1805 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -1,5 +1,6 @@ package kr.touroot.travelogue.fixture; +import java.time.LocalDate; import java.util.List; import kr.touroot.travelogue.dto.response.TravelogueDayResponse; import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; @@ -19,6 +20,8 @@ public static TravelogueResponse getTravelogueResponse() { return TravelogueResponse.builder() .id(1L) .title("제주에 하영 옵서") + .createdAt(LocalDate.now()) + .authorNickname("리비") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .build(); @@ -28,6 +31,8 @@ public static Page getTravelogueResponses() { return new PageImpl<>(List.of(TravelogueResponse.builder() .id(1L) .title("제주에 하영 옵서") + .createdAt(LocalDate.now()) + .authorNickname("리비") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .build())); @@ -54,6 +59,7 @@ public static List getTraveloguePlaceResponses() { public static TraveloguePositionResponse getTraveloguePositionResponse() { return TraveloguePositionResponse.builder() + .id(1L) .lat("34.54343") .lng("126.66977") .build(); diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 3a508ece..a83ac41f 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -110,7 +110,7 @@ public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { } public Member initKakaoMemberTestData() { - Member member = new Member(1L, "tester", "http://image.com", LoginType.KAKAO); + Member member = new Member(1L, "리비", "http://image.com", LoginType.KAKAO); return memberRepository.save(member); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index 529728bc..166c05a1 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import java.time.LocalDate; import java.util.UUID; @@ -74,19 +75,22 @@ void createTravelPlanWithInvalidLengthTitle() { .hasMessage("여행 계획은 1자 이상, 20자 이하여야 합니다"); } - @DisplayName("여행 계획은 지난 날짜를 검증할 시 예외가 발생한다.") + @DisplayName("여행 계획의 시작날짜와 특정 날짜를 비교할 수 있다.") @Test void validateStartDate() { - TravelPlan travelPlan = new TravelPlan(VALID_TITLE, LocalDate.MIN, VALID_UUID, VALID_AUTHOR); + // given + LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDate today = LocalDate.now(); + LocalDate tommorow = LocalDate.now().plusDays(1); - assertThatCode(travelPlan::validateStartDate) - .isInstanceOf(BadRequestException.class) - .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); - // when - boolean actual = travelPlan.isValidStartDate(); + Member author = new Member(1L, 1L, null, null, "tester", "http://url.com", LoginType.KAKAO); + TravelPlan todayPlan = new TravelPlan("test", today, VALID_UUID, author); - // then - assertThat(actual).isFalse(); + // when & then + assertAll( + () -> assertThat(todayPlan.isStartDateBefore(tommorow)).isTrue(), + () -> assertThat(todayPlan.isStartDateBefore(yesterday)).isFalse() + ); } @DisplayName("여행 계획은 작성자가 아닌 사용자가 검증을 시도하면 예외가 발생한다.") @@ -94,7 +98,7 @@ void validateStartDate() { void validateAuthor() { // given Member author = new Member(1L, 1L, null, null, "tester", "http://url.com", LoginType.KAKAO); - TravelPlan travelPlan = new TravelPlan("test", LocalDate.MIN, VALID_UUID, author); + TravelPlan travelPlan = new TravelPlan("test", LocalDate.now(), VALID_UUID, author); Member notAuthor = new Member(2L, 2L, null, null, "tester2", "http://url.com", LoginType.KAKAO); // when diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index b4e4b9ad..2d17fb2f 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -1,6 +1,7 @@ package kr.touroot.travelplan.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; @@ -108,6 +109,28 @@ void createTravelPlanWithInvalidStartDate() { .hasMessage("지난 날짜에 대한 계획은 작성할 수 없습니다."); } + @DisplayName("당일에 시작하는 여행 계획을 생성할 수 있다") + @Test + void createTravelPlanStartsAtToday() { + // given + PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); + PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + .placeName("잠실한강공원") + .description("신나는 여행 장소") + .position(locationRequest) + .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); + TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.now()) + .days(List.of(planDayCreateRequest)) + .build(); + + // when & then= + assertThatCode(() -> travelPlanService.createTravelPlan(request, memberAuth)) + .doesNotThrowAnyException(); + } + @DisplayName("여행 계획 서비스는 여행 계획 조회 시 상세 정보를 반환한다.") @Test void readTravelPlan() { From 49d07afe8ccc6759a6b8e5f618edf96db92482b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:52:19 +0900 Subject: [PATCH 082/108] =?UTF-8?q?[Feature]=20-=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=9A=A9=EB=9F=89=20?= =?UTF-8?q?=EC=A6=9D=EA=B0=80=20(#246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index a54e2b2e..cbc56cd3 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -55,8 +55,8 @@ server: spring: servlet: multipart: - max-file-size: 5MB - max-request-size: 50MB + max-file-size: 25MB + max-request-size: 250MB config: activate: on-profile: dev From 645a2364938dcf59daa527006064f56982dbd4b7 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 7 Aug 2024 16:22:35 +0900 Subject: [PATCH 083/108] =?UTF-8?q?[Refactor]=20-=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=AC=ED=96=89=EA=B8=B0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20URL=20=EC=B6=94=EA=B0=80=20(#252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: Travelogue 응답에 작성자 프로필 사진 URL 추가 * refactor: TravelogueResponse 사양 변경에 따른 test fixture 및 helper 수정 --- .../java/kr/touroot/travelogue/domain/Travelogue.java | 4 ++++ .../travelogue/dto/response/TravelogueResponse.java | 5 ++++- .../touroot/authentication/fixture/MemberFixture.java | 10 ++++++++-- .../travelogue/fixture/TravelogueResponseFixture.java | 2 ++ .../travelogue/helper/TravelogueTestHelper.java | 2 +- .../travelplan/helper/TravelPlanTestHelper.java | 2 +- 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index a193891a..8d512f1e 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -97,4 +97,8 @@ public boolean isAuthor(Member author) { public String getAuthorNickname() { return author.getNickname(); } + + public String getAuthorProfileImageUrl() { + return author.getProfileImageUrl(); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index 0438d1fd..50935f80 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -14,7 +14,9 @@ public record TravelogueResponse( String title, @Schema(description = "작성자 닉네임", example = "지니") String authorNickname, - @Schema(description = "여행기 썸네일 링크", example = "https://썸네일.png") + @Schema(description = "작성자 프로필 사진 URL", example = "https://dev.touroot.kr/images/profile.png") + String authorProfileImageUrl, + @Schema(description = "여행기 썸네일 링크", example = "https://dev.touroot.kr/images/thumbnail.png") String thumbnail, @Schema(description = "작성 날짜") LocalDate createdAt, @@ -27,6 +29,7 @@ public static TravelogueResponse of(Travelogue travelogue, List getTravelogueResponses() { .title("제주에 하영 옵서") .createdAt(LocalDate.now()) .authorNickname("리비") + .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .build())); diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index a83ac41f..5470ed97 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -110,7 +110,7 @@ public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { } public Member initKakaoMemberTestData() { - Member member = new Member(1L, "리비", "http://image.com", LoginType.KAKAO); + Member member = new Member(1L, "리비", "https://dev.touroot.kr/temporary/profile.png", LoginType.KAKAO); return memberRepository.save(member); } } diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 4795a186..27b919f1 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -90,7 +90,7 @@ public TravelPlan initTravelPlanTestData(Member author) { } public Member initMemberTestData() { - Member member = getKakaoMember(1L, "tester", "http://image.com"); + Member member = getKakaoMember(1L, "tester", "https://dev.touroot.kr/temporary/profile.png"); return memberRepository.save(member); } } From 3b600fa49859240cf22630f5f3f8538f8181048e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 8 Aug 2024 10:18:17 +0900 Subject: [PATCH 084/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EC=97=90=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 메인 페이지 조회 시 Response 변경 * feat: 여행기 상세 조회 시 로그인/비로그인 구분 추가 * fix: 여행기 상세 조회 방식 원래대로 원복 * feat: 로그인 완료 DTO 응답 필드 추가 - memberId * feat: 여행기 상세 조회 응답 DTO 필드 추가 - authorId: 작성자 Id * feat: TravelogueSimpleResponse DTO 스웨거 문서작업 추가 --------- Co-authored-by: eunjungL --- .../dto/response/LoginResponse.java | 10 ++++++- .../controller/TravelogueController.java | 5 ++-- .../touroot/travelogue/domain/Travelogue.java | 4 +++ .../dto/response/TravelogueResponse.java | 3 ++ .../response/TravelogueSimpleResponse.java | 29 +++++++++++++++++++ .../service/TravelogueFacadeService.java | 5 ++-- .../controller/LoginControllerTest.java | 2 +- .../controller/TravelogueControllerTest.java | 3 +- .../fixture/TravelogueResponseFixture.java | 10 +++---- .../service/TravelogueFacadeServiceTest.java | 6 ++-- 10 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueSimpleResponse.java diff --git a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java index 3f88cc6d..f181a4ff 100644 --- a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java @@ -2,13 +2,21 @@ import io.swagger.v3.oas.annotations.media.Schema; import kr.touroot.member.domain.Member; +import lombok.Builder; +@Builder public record LoginResponse( + @Schema(description = "로그인한 유저의 PK", example = "1") Long memberId, @Schema(description = "로그인된 유저의 닉네임", example = "리비") String nickname, @Schema(description = "로그인된 유저의 프로필 이미지 경로", example = "https://img-ul") String profileImageUrl, @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") String accessToken) { public static LoginResponse of(Member member, String accessToken) { - return new LoginResponse(member.getNickname(), member.getProfileImageUrl(), accessToken); + return LoginResponse.builder() + .memberId(member.getId()) + .nickname(member.getNickname()) + .profileImageUrl(member.getProfileImageUrl()) + .accessToken(accessToken) + .build(); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index 576fa55f..88030b9b 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -13,6 +13,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.service.TravelogueFacadeService; import lombok.RequiredArgsConstructor; import org.springdoc.core.converters.models.PageableAsQueryParam; @@ -91,12 +92,12 @@ public ResponseEntity findTravelogue(@PathVariable Long id) }) @PageableAsQueryParam @GetMapping - public ResponseEntity> findMainPageTravelogues( + public ResponseEntity> findMainPageTravelogues( @Parameter(hidden = true) @PageableDefault(size = 5, sort = "id", direction = Direction.DESC) Pageable pageable ) { - return ResponseEntity.ok(travelogueFacadeService.findTravelogues(pageable)); + return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(pageable)); } @Operation(summary = "여행기 삭제") diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index 8d512f1e..b4c2c95b 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -101,4 +101,8 @@ public String getAuthorNickname() { public String getAuthorProfileImageUrl() { return author.getProfileImageUrl(); } + + public Long getAuthorId() { + return author.getId(); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index 50935f80..feb71e2a 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -12,6 +12,8 @@ public record TravelogueResponse( Long id, @Schema(description = "여행기 제목", example = "서울 강남 여행기") String title, + @Schema(description = "작성자 ID", example = "1") + Long authorId, @Schema(description = "작성자 닉네임", example = "지니") String authorNickname, @Schema(description = "작성자 프로필 사진 URL", example = "https://dev.touroot.kr/images/profile.png") @@ -28,6 +30,7 @@ public static TravelogueResponse of(Travelogue travelogue, List findTravelogues(final Pageable pageable) { + public Page findSimpleTravelogues(final Pageable pageable) { Page travelogues = travelogueService.findAll(pageable); return new PageImpl<>(travelogues.stream() - .map(travelogue -> TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue))) + .map(TravelogueSimpleResponse::from) .toList()); } diff --git a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java index ea296869..e793c929 100644 --- a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java @@ -39,7 +39,7 @@ class LoginControllerTest { @DisplayName("카카오 로그인 요청을 처리할 수 있다") @Test void loginTest() throws Exception { - LoginResponse loginResponse = new LoginResponse("리비", "img-url", "test-access-token"); + LoginResponse loginResponse = new LoginResponse(1L, "리비", "img-url", "test-access-token"); when(loginService.login(any(String.class), any(String.class))).thenReturn(loginResponse); mockMvc.perform(post("/api/v1/login/oauth/kakao") diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 9ea1eb04..eca8b377 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -14,6 +14,7 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.fixture.TravelogueResponseFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; @@ -114,7 +115,7 @@ void findTravelogue() throws JsonProcessingException { @Test void findMainPageTravelogues() throws JsonProcessingException { testHelper.initTravelogueTestData(); - Page responses = TravelogueResponseFixture.getTravelogueResponses(); + Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); RestAssured.given().log().all() .accept(ContentType.JSON) diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 489c0ddd..961c43eb 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -6,6 +6,7 @@ import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; import kr.touroot.travelogue.dto.response.TraveloguePositionResponse; import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.stereotype.Component; @@ -22,21 +23,20 @@ public static TravelogueResponse getTravelogueResponse() { .title("제주에 하영 옵서") .createdAt(LocalDate.now()) .authorNickname("리비") + .authorId(1L) .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .build(); } - public static Page getTravelogueResponses() { - return new PageImpl<>(List.of(TravelogueResponse.builder() + public static Page getTravelogueSimpleResponses() { + return new PageImpl<>(List.of(TravelogueSimpleResponse.builder() .id(1L) .title("제주에 하영 옵서") - .createdAt(LocalDate.now()) .authorNickname("리비") - .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") + .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") - .days(getTravelogueDayResponses()) .build())); } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index aff72709..472f528b 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -12,7 +12,7 @@ import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.service.MemberService; import kr.touroot.travelogue.dto.request.TravelogueRequest; -import kr.touroot.travelogue.dto.response.TravelogueResponse; +import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.fixture.TravelogueResponseFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; @@ -97,9 +97,9 @@ void findTravelogueById() { @Test void findTravelogues() { testHelper.initTravelogueTestData(); - Page responses = TravelogueResponseFixture.getTravelogueResponses(); + Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); - assertThat(service.findTravelogues(Pageable.ofSize(5))) + assertThat(service.findSimpleTravelogues(Pageable.ofSize(5))) .isEqualTo(responses); } From eb565d45e30d2ee185e93158b1d1f39458f41ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:03:47 +0900 Subject: [PATCH 085/108] =?UTF-8?q?[Feature]=20-=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EC=8B=9C=20InternalServerError=20?= =?UTF-8?q?=EB=8A=94=20=EC=8A=A4=ED=83=9D=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EB=8F=84=20=EA=B0=99=EC=9D=B4=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(#264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 예외 처리 시 InternalServerError 는 stackTrack를 로깅하는 기능 구현 * feat: Swagger 에서 사용자의 프로필 이미지 url example을 실제 url로 변경 * feat: 서버에서 발생한 에러 메시지를 사용자에게 숨기도록 구현 --- .../authentication/dto/response/LoginResponse.java | 12 ++++++++---- .../global/exception/GlobalExceptionHandler.java | 11 ++++++++++- .../kr/touroot/member/dto/request/MemberRequest.java | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java index f181a4ff..fe22ab76 100644 --- a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java @@ -6,10 +6,14 @@ @Builder public record LoginResponse( - @Schema(description = "로그인한 유저의 PK", example = "1") Long memberId, - @Schema(description = "로그인된 유저의 닉네임", example = "리비") String nickname, - @Schema(description = "로그인된 유저의 프로필 이미지 경로", example = "https://img-ul") String profileImageUrl, - @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") String accessToken) { + @Schema(description = "로그인한 유저의 PK", example = "1") + Long memberId, + @Schema(description = "로그인된 유저의 닉네임", example = "리비") + String nickname, + @Schema(description = "로그인된 유저의 프로필 이미지 경로", example = "https://dev.touroot.kr/profile-image-ex.png") + String profileImageUrl, + @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") + String accessToken) { public static LoginResponse of(Member member, String accessToken) { return LoginResponse.builder() diff --git a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index 33f52fc6..d438819a 100644 --- a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -39,7 +39,7 @@ public ResponseEntity handleMethodArgumentNotValidException( @ExceptionHandler(ClientException.class) public ResponseEntity handleClientException(ClientException exception) { - log.error("CLIENT_EXCEPTION :: message = {}", exception.getMessage()); + log.error("CLIENT_EXCEPTION :: stackTrace = {}", exception.getStackTrace()); ExceptionResponse data = new ExceptionResponse(exception.getMessage()); return ResponseEntity.internalServerError().body(data); @@ -61,4 +61,13 @@ public ResponseEntity handleForbiddenException(ForbiddenExcep return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(data); } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception) { + log.error("EXCEPTION :: stackTrace = {}", exception.getStackTrace()); + + ExceptionResponse data = new ExceptionResponse("서버에 문제가 발생했습니다. 투룻에 문의해 주세요."); + return ResponseEntity.internalServerError() + .body(data); + } } diff --git a/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java b/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java index 97ebaae3..8708f751 100644 --- a/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java +++ b/backend/src/main/java/kr/touroot/member/dto/request/MemberRequest.java @@ -17,7 +17,7 @@ public record MemberRequest( @Schema(description = "사용자 닉네임", example = "뚜리") @NotBlank(message = "닉네임은 비어있을 수 없습니다.") String nickname, - @Schema(description = "사용자 프로필 사진 URL", example = "S3 이미지 URL") + @Schema(description = "사용자 프로필 사진 URL", example = "https://dev.touroot.kr/profile-image-ex.png") @NotBlank(message = "프로필 사진 URL은 비어있을 수 없습니다.") String profileImageUrl ) { From 04d4cb52b20a8679295bde35132be566a87a7660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Thu, 8 Aug 2024 17:30:34 +0900 Subject: [PATCH 086/108] =?UTF-8?q?[Fix]=20-=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EC=9D=B4=20255=EC=9E=90=EB=A5=BC=20=EB=84=98?= =?UTF-8?q?=EC=96=B4=EA=B0=80=EB=8A=94=20=EC=97=AC=ED=96=89=EA=B8=B0,=20?= =?UTF-8?q?=EC=97=AC=ED=96=89=20=EA=B3=84=ED=9A=8D=EC=9D=B4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/kr/touroot/travelogue/domain/TraveloguePlace.java | 1 + .../main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index 1385a258..a8135fc6 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -35,6 +35,7 @@ public class TraveloguePlace extends BaseEntity { @Column(name = "PLACE_ORDER", nullable = false) private Integer order; + @Column(columnDefinition = "VARCHAR(300)") private String description; @JoinColumn(nullable = false) diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index 44b6ed6b..15808cd1 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -30,6 +30,7 @@ public class TravelPlanPlace extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(columnDefinition = "VARCHAR(300)") private String description; @Column(name = "PLAN_PLACE_ORDER", nullable = false) From 63cf2733d683b15bef83376041f3539d03579233 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Thu, 8 Aug 2024 17:33:16 +0900 Subject: [PATCH 087/108] =?UTF-8?q?[Fix]=20-=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=B5=9C=EB=8C=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EA=B2=80=EC=A6=9D=20=EB=88=84=EB=9D=BD=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 여행기 작성 시, 최대 업로드 가능 개수 이상의 사진이 추가되면 예외 발생하도록 검증 추가 * refactor: 검증 로직 추가에 따른 테스트 코드 수정 --- .../dto/request/TraveloguePlaceRequest.java | 1 + .../controller/TravelogueControllerTest.java | 40 ++++++++++++++++++- .../fixture/TravelogueRequestFixture.java | 30 ++++++++++---- .../service/TravelogueDayServiceTest.java | 5 ++- .../service/TravelogueFacadeServiceTest.java | 11 ++++- .../service/TraveloguePlaceServiceTest.java | 3 +- .../service/TravelogueServiceTest.java | 9 ++++- 7 files changed, 85 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index f7d8ec63..51a6bf60 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -24,6 +24,7 @@ public record TraveloguePlaceRequest( String description, @Schema(description = "여행기 장소 사진") @NotNull(message = "여행기 장소 사진은 비어있을 수 없습니다.") + @Size(message = "여행기 장소 사진은 최대 10개입니다.", max = 10) @Valid List photoUrls ) { diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index eca8b377..bb9d3dea 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -7,11 +7,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import java.util.List; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; +import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; @@ -70,7 +75,10 @@ void createTravelogue() { Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); - TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); Member member = testHelper.initKakaoMemberTestData(); String accessToken = jwtTokenProvider.createToken(member.getId()); @@ -84,10 +92,38 @@ void createTravelogue() { .header("Location", "/api/v1/travelogues/1"); } + @DisplayName("최대 업로드 가능 개수 이상의 사진을 포함한 여행기를 작성하면 예외가 발생한다.") + @Test + void createTravelogueWithOver10PhotosEachPlaces() throws JsonProcessingException { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + List photos = TravelogueRequestFixture.getTraveloguePhotoRequestsOverLimit(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); + Member member = testHelper.initKakaoMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + ExceptionResponse response = new ExceptionResponse("여행기 장소 사진은 최대 10개입니다."); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().post("/api/v1/travelogues") + .then().log().all() + .statusCode(400).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + @DisplayName("여행기를 작성할 때 로그인 되어 있지 않으면 예외가 발생한다.") @Test void createTravelogueWithNotLoginThrowException() { - TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); RestAssured.given().log().all() .contentType(ContentType.JSON) diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index ef8cd2cb..03b4cdf1 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -1,12 +1,12 @@ package kr.touroot.travelogue.fixture; import java.util.List; -import org.springframework.stereotype.Component; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TraveloguePositionRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; +import org.springframework.stereotype.Component; @Component public class TravelogueRequestFixture { @@ -14,20 +14,20 @@ public class TravelogueRequestFixture { private TravelogueRequestFixture() { } - public static TravelogueRequest getTravelogueRequest() { - return new TravelogueRequest("제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png", getTravelogueDayRequests()); + public static TravelogueRequest getTravelogueRequest(List days) { + return new TravelogueRequest("제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png", days); } - public static List getTravelogueDayRequests() { - return List.of(new TravelogueDayRequest(getTraveloguePlaceRequests())); + public static List getTravelogueDayRequests(List places) { + return List.of(new TravelogueDayRequest(places)); } - public static List getTraveloguePlaceRequests() { + public static List getTraveloguePlaceRequests(List photos) { return List.of(new TraveloguePlaceRequest( "함덕해수욕장", getTraveloguePositionRequest(), "에메랄드 빛 해변", - getTraveloguePhotoRequests() + photos )); } @@ -38,4 +38,20 @@ public static TraveloguePositionRequest getTraveloguePositionRequest() { public static List getTraveloguePhotoRequests() { return List.of(new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image1.png")); } + + public static List getTraveloguePhotoRequestsOverLimit() { + return List.of( + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image1.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image2.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image3.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image4.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image5.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image6.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image7.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image8.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image9.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image10.png"), + new TraveloguePhotoRequest("https://dev.touroot.kr/temporary/image11.png") + ); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java index 22961ae1..1e7cde22 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -13,6 +13,7 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; @@ -55,7 +56,9 @@ void setUp() { @DisplayName("여행기의 일자들을 생성한다.") @Test void createDays() { - List requests = TravelogueRequestFixture.getTravelogueDayRequests(); + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List requests = TravelogueRequestFixture.getTravelogueDayRequests(places); Member author = testHelper.persistMember(); Travelogue travelogue = testHelper.persistTravelogue(author); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 472f528b..59a9f488 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; +import java.util.List; import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; @@ -11,6 +12,9 @@ import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.service.MemberService; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; @@ -68,8 +72,11 @@ void setUp() { @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List days = TravelogueRequestFixture.getTravelogueDayRequests(places); when(s3Provider.copyImageToPermanentStorage( - TravelogueRequestFixture.getTravelogueRequest().thumbnail()) + TravelogueRequestFixture.getTravelogueRequest(days).thumbnail()) ).thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); when(s3Provider.copyImageToPermanentStorage( TravelogueRequestFixture.getTraveloguePhotoRequests().get(0).url()) @@ -78,7 +85,7 @@ void createTravelogue() { testHelper.initKakaoMemberTestData(); MemberAuth memberAuth = new MemberAuth(1L); - TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); assertThat(service.createTravelogue(memberAuth, request)) .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 3504abc3..60e01d75 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -57,7 +57,8 @@ void setUp() { @DisplayName("여행기 장소를 생성한다.") @Test void createPlaces() { - List requests = TravelogueRequestFixture.getTraveloguePlaceRequests(); + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List requests = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); Member author = testHelper.persistMember(); Travelogue travelogue = testHelper.persistTravelogue(author); TravelogueDay day = testHelper.persistTravelogueDay(travelogue); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index cff180c9..fc8260e0 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -6,12 +6,16 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; +import java.util.List; import kr.touroot.global.ServiceTest; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.dto.request.TravelogueDayRequest; +import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; +import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.fixture.TravelogueResponseFixture; @@ -64,7 +68,10 @@ void createTravelogue() { .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); Member author = testHelper.initKakaoMemberTestData(); - TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(); + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); Travelogue createdTravelogue = travelogueService.createTravelogue(author, request); From 28733c97fccd7f7cac653bde5afe472cb2c60661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Thu, 8 Aug 2024 22:03:33 +0900 Subject: [PATCH 088/108] =?UTF-8?q?[Fix]=20-=20InternalServerError=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EC=8B=9C=20=EC=98=88=EC=99=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=ED=8A=B8=EB=A0=88=EC=9D=B4=EC=8A=A4=EA=B0=80=20?= =?UTF-8?q?=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EB=A1=9C=EA=B9=85=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/touroot/global/exception/GlobalExceptionHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index d438819a..61ca06b0 100644 --- a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -39,7 +39,7 @@ public ResponseEntity handleMethodArgumentNotValidException( @ExceptionHandler(ClientException.class) public ResponseEntity handleClientException(ClientException exception) { - log.error("CLIENT_EXCEPTION :: stackTrace = {}", exception.getStackTrace()); + log.error("CLIENT_EXCEPTION :: stackTrace = ", exception); ExceptionResponse data = new ExceptionResponse(exception.getMessage()); return ResponseEntity.internalServerError().body(data); @@ -64,7 +64,7 @@ public ResponseEntity handleForbiddenException(ForbiddenExcep @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception exception) { - log.error("EXCEPTION :: stackTrace = {}", exception.getStackTrace()); + log.error("EXCEPTION :: stackTrace = ", exception); ExceptionResponse data = new ExceptionResponse("서버에 문제가 발생했습니다. 투룻에 문의해 주세요."); return ResponseEntity.internalServerError() From da69b694afd87f47491bfb160a9c0765e4e7fa64 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:23:12 +0900 Subject: [PATCH 089/108] =?UTF-8?q?[Fix]=20-=20S3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=98=A4=EB=A5=98=20=EC=98=88=EC=99=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EB=B3=80=EA=B2=BD=20(#284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: S3 관련 예외 발생 시 S3UploadException으로 변경 --- .../global/exception/GlobalExceptionHandler.java | 9 +++++++++ .../touroot/global/exception/S3UploadException.java | 8 ++++++++ .../touroot/image/infrastructure/AwsS3Provider.java | 12 ++++++------ .../image/infrastructure/AwsS3ProviderTest.java | 4 ++-- 4 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/global/exception/S3UploadException.java diff --git a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index 61ca06b0..a31df532 100644 --- a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -70,4 +70,13 @@ public ResponseEntity handleException(Exception exception) { return ResponseEntity.internalServerError() .body(data); } + + @ExceptionHandler(S3UploadException.class) + public ResponseEntity handleS3UploadException(S3UploadException exception) { + log.warn("S3_UPLOAD_EXCEPTION :: message = {}", exception.getMessage()); + + ExceptionResponse data = new ExceptionResponse("이미지 업로드에 실패했습니다."); + return ResponseEntity.badRequest() + .body(data); + } } diff --git a/backend/src/main/java/kr/touroot/global/exception/S3UploadException.java b/backend/src/main/java/kr/touroot/global/exception/S3UploadException.java new file mode 100644 index 00000000..42e48d5b --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/exception/S3UploadException.java @@ -0,0 +1,8 @@ +package kr.touroot.global.exception; + +public class S3UploadException extends RuntimeException { + + public S3UploadException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java index d4b9e374..7a785d29 100644 --- a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java @@ -4,7 +4,7 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; -import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.S3UploadException; import kr.touroot.image.domain.ImageFile; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -79,8 +79,8 @@ private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) .build(); s3Client.putObject(putObjectRequest, requestBody); - } catch (IOException e) { - throw new BadRequestException("파일 저장에 실패했습니다."); + } catch (IOException exception) { + throw new S3UploadException("S3에 이미지를 업로드하다 오류가 발생했습니다."); } } @@ -95,7 +95,7 @@ public String copyImageToPermanentStorage(String imageUrl) { private void validateS3Path(String imageKey) { if (!imageKey.startsWith(imageBaseUri + temporaryStoragePath)) { - throw new BadRequestException("S3 이미지 url 형식이 잘못되었습니다."); + throw new S3UploadException("S3 이미지 url 형식이 잘못되었습니다."); } } @@ -109,8 +109,8 @@ private void copyFile(String sourceKey, String destinationKey) { .build(); s3Client.copyObject(request); - } catch (NoSuchKeyException e) { - throw new BadRequestException("복사하려는 사진이 존재하지 않습니다."); + } catch (NoSuchKeyException exception) { + throw new S3UploadException("S3 버킷에 복사하려는 사진이 존재하지 않습니다."); } } } diff --git a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java index f7730917..3d8ce5b1 100644 --- a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java +++ b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java @@ -5,7 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.S3UploadException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -54,7 +54,7 @@ void copyImageToPermanentStorage() { void copyImageToPermanentStorageWithInvalidPath() { String imageUrl = "invalid/testUrl.png"; assertThatThrownBy(() -> s3Provider.copyImageToPermanentStorage(imageUrl)) - .isInstanceOf(BadRequestException.class) + .isInstanceOf(S3UploadException.class) .hasMessage("S3 이미지 url 형식이 잘못되었습니다."); } } From f7ec5abe4f7cd80b95fe888159b7fe4b38ec598e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:54:02 +0900 Subject: [PATCH 090/108] =?UTF-8?q?[Feature]=20-=20=EC=99=B8=EB=B6=80=20AP?= =?UTF-8?q?I=20request/response=20=EB=A1=9C=EA=B9=85=20(#286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 카카오 로그인 과정에서 요청/응답 로깅 기능 구현 * refactor: 외부 API 요청/응답 로깅 메시지 구분 * refactor: 2xx 응답에 대해서만 로깅하도록 변경 * refactor: 2xx 응답에 대해서만 로깅하도록 변경 --- .../infrastructure/KakaoOauthClient.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java index b3eb1459..a7bf6bc6 100644 --- a/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/KakaoOauthClient.java @@ -6,6 +6,7 @@ import kr.touroot.authentication.dto.response.kakao.KakaoAccessTokenResponse; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ClientException; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; @@ -20,6 +21,7 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; +@Slf4j @Component public class KakaoOauthClient { @@ -59,6 +61,7 @@ public OauthUserInformationResponse requestUserInformation(String authorizationC .header(HttpHeaders.AUTHORIZATION, "Bearer " + kakaoAccessTokenResponse.accessToken()) .retrieve() .onStatus(HttpStatusCode::isError, this::handleClientError) + .onStatus(HttpStatusCode::is2xxSuccessful, this::handleSuccessLogging) .toEntity(OauthUserInformationResponse.class) .getBody(); } @@ -76,14 +79,21 @@ private KakaoAccessTokenResponse requestAccessToken(String authorizationCode, St .body(params) .retrieve() .onStatus(HttpStatusCode::isError, this::handleClientError) + .onStatus(HttpStatusCode::is2xxSuccessful, this::handleSuccessLogging) .toEntity(KakaoAccessTokenResponse.class) .getBody(); } private void handleClientError(HttpRequest request, ClientHttpResponse response) throws IOException { + log.error("KakaoOauth:: {} {} ({})", request.getMethod(), request.getURI(), response.getStatusCode()); + if (response.getStatusCode().is4xxClientError()) { throw new BadRequestException("잘못된 로그인 요청입니다. 인가코드를 확인해주세요"); } throw new ClientException("외부 서비스의 장애로 카카오로그인을 이용할 수 없습니다"); } + + private void handleSuccessLogging(HttpRequest request, ClientHttpResponse response) throws IOException { + log.info("KakaoOauth:: {} {} ({})", request.getMethod(), request.getURI(), response.getStatusCode()); + } } From ff0ecf19a8354c5b46874e3bfa8b8b4ec3dd1d5a Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Fri, 9 Aug 2024 13:58:22 +0900 Subject: [PATCH 091/108] =?UTF-8?q?[Fix]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=EA=B0=80=20=ED=95=98=EB=82=98=EB=8F=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20?= =?UTF-8?q?=EC=97=AC=ED=96=89=EA=B8=B0=EA=B0=80=20=EC=A0=95=EC=83=81=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EB=90=98=EB=B2=84=EB=A6=AC=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=B0=9C=EC=83=9D=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 여행기 작성 시 요청 DTO 제약 조건 수정 * test: 변경된 여행기 작성 제약 조건에 따른 테스트 작성 --- .../dto/request/TravelogueDayRequest.java | 2 + .../dto/request/TraveloguePlaceRequest.java | 1 - .../dto/request/TravelogueRequest.java | 1 + .../controller/TravelogueControllerTest.java | 45 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java index da5737e0..bbdf371f 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueDayRequest.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import java.util.List; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; @@ -10,6 +11,7 @@ public record TravelogueDayRequest( @Schema(description = "여행기 장소 목록") @NotNull(message = "여행기 장소 목록은 비어있을 수 없습니다.") + @Size(message = "여행기 장소는 최소 한 곳은 포함되어야 합니다.", min = 1) @Valid List places ) { diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java index 51a6bf60..7fef381d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TraveloguePlaceRequest.java @@ -23,7 +23,6 @@ public record TraveloguePlaceRequest( @Size(message = "장소 설명은 300글자 이하입니다.", max = 300) String description, @Schema(description = "여행기 장소 사진") - @NotNull(message = "여행기 장소 사진은 비어있을 수 없습니다.") @Size(message = "여행기 장소 사진은 최대 10개입니다.", max = 10) @Valid List photoUrls diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 7b9b8fcf..08a2cf52 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -19,6 +19,7 @@ public record TravelogueRequest( String thumbnail, @Schema(description = "여행기 일자 목록") @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") + @Size(message = "여행기 일자는 최소 1일은 포함되어야 합니다.", min = 1) @Valid List days ) { diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index bb9d3dea..b28ec55c 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -117,6 +117,51 @@ void createTravelogueWithOver10PhotosEachPlaces() throws JsonProcessingException .body(is(objectMapper.writeValueAsString(response))); } + @DisplayName("최소 여행 일자 개수를 만족하지 않은 여행기를 작성하려하면 예외가 발생한다.") + @Test + void createTravelogueWithNoDays() throws JsonProcessingException { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(List.of()); + Member member = testHelper.initKakaoMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + ExceptionResponse response = new ExceptionResponse("여행기 일자는 최소 1일은 포함되어야 합니다."); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().post("/api/v1/travelogues") + .then().log().all() + .statusCode(400).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + + @DisplayName("최소 여행 장소 개수를 만족하지 않은 여행기를 작성하려하면 예외가 발생한다.") + @Test + void createTravelogueWithNoPlacesDay() throws JsonProcessingException { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + List days = TravelogueRequestFixture.getTravelogueDayRequests(List.of()); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); + Member member = testHelper.initKakaoMemberTestData(); + String accessToken = jwtTokenProvider.createToken(member.getId()); + + ExceptionResponse response = new ExceptionResponse("여행기 장소는 최소 한 곳은 포함되어야 합니다."); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().post("/api/v1/travelogues") + .then().log().all() + .statusCode(400).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + @DisplayName("여행기를 작성할 때 로그인 되어 있지 않으면 예외가 발생한다.") @Test void createTravelogueWithNotLoginThrowException() { From 33edbe738383b02fa2a410d5a2db276b9fdfd611 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Fri, 9 Aug 2024 14:16:56 +0900 Subject: [PATCH 092/108] =?UTF-8?q?[Fix]=20-=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EB=B0=8F=20=EC=A0=81=EC=A0=88=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EA=B0=9D=EC=B2=B4=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EC=A0=95=20(#291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 메소드 반환 타입 구체화 * chore: 메소드 이름 오타 수정 --- .../java/kr/touroot/member/repository/MemberRepository.java | 2 +- .../main/java/kr/touroot/member/service/MemberService.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java b/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java index 62bfaf2d..7ccfa3ea 100644 --- a/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java +++ b/backend/src/main/java/kr/touroot/member/repository/MemberRepository.java @@ -12,5 +12,5 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); - Optional findByNickname(String nickname); + Optional findByNickname(String nickname); } diff --git a/backend/src/main/java/kr/touroot/member/service/MemberService.java b/backend/src/main/java/kr/touroot/member/service/MemberService.java index 4a3a8802..89ba31dd 100644 --- a/backend/src/main/java/kr/touroot/member/service/MemberService.java +++ b/backend/src/main/java/kr/touroot/member/service/MemberService.java @@ -30,7 +30,7 @@ public Long createMember(MemberRequest request) { private void validateRequest(MemberRequest request) { validateEmailDuplication(request.email()); - validateNicknameDuplicationr(request.nickname()); + validateNicknameDuplication(request.nickname()); } private void validateEmailDuplication(String email) { @@ -39,7 +39,7 @@ private void validateEmailDuplication(String email) { } } - private void validateNicknameDuplicationr(String nickname) { + private void validateNicknameDuplication(String nickname) { if (memberRepository.findByNickname(nickname).isPresent()) { throw new BadRequestException("이미 사용 중인 닉네임입니다."); } From 7b0d4c5a513f33ae6c7aa645b732915370f46b40 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:41:40 +0900 Subject: [PATCH 093/108] =?UTF-8?q?[Feature]=20-=20refresh=20token=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20(#293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 로그인 응답에 Refresh Token 추가 * feat: refresh token으로 토큰 재발급 구현 * docs: 재로그인 swagger 작성 * test: 리프레시 토큰 재로그인 테스트 추가 * style: application.yml의 jwt 만료 시간에 주석 추가 --- .../controller/LoginController.java | 18 ++++ .../dto/request/TokenReissueRequest.java | 10 ++ .../dto/response/LoginResponse.java | 16 ++-- .../dto/response/TokenResponse.java | 4 + .../infrastructure/JwtTokenProvider.java | 39 ++++++-- .../authentication/service/LoginService.java | 9 ++ .../kr/touroot/global/auth/JwtAuthFilter.java | 2 +- .../exception/GlobalExceptionHandler.java | 9 ++ backend/src/main/resources/application.yml | 12 ++- .../controller/LoginControllerTest.java | 91 +++++++++++++------ .../helper/LoginTestHelper.java | 19 ++++ .../infrastructure/JwtTokenProviderTest.java | 10 +- .../service/LoginServiceTest.java | 27 +++++- .../controller/MyPageControllerTest.java | 3 +- .../controller/TravelogueControllerTest.java | 26 +++--- .../controller/TravelPlanControllerTest.java | 16 ++-- .../src/test/resources/application-test.yml | 5 +- 17 files changed, 237 insertions(+), 79 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/authentication/dto/request/TokenReissueRequest.java create mode 100644 backend/src/main/java/kr/touroot/authentication/dto/response/TokenResponse.java create mode 100644 backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java diff --git a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java index 12b8e6e0..2d7caec1 100644 --- a/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java +++ b/backend/src/main/java/kr/touroot/authentication/controller/LoginController.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import kr.touroot.authentication.dto.request.LoginRequest; +import kr.touroot.authentication.dto.request.TokenReissueRequest; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.service.LoginService; import kr.touroot.global.exception.dto.ExceptionResponse; @@ -65,4 +66,21 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest requ return ResponseEntity.ok() .body(loginService.login(request)); } + + @Operation(summary = "리프레스 토큰으로 재로그인") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 refresh token이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PostMapping("/reissue-token") + public ResponseEntity reissueToken(@Valid @RequestBody TokenReissueRequest request) { + return ResponseEntity.ok(loginService.reissueToken(request)); + } } diff --git a/backend/src/main/java/kr/touroot/authentication/dto/request/TokenReissueRequest.java b/backend/src/main/java/kr/touroot/authentication/dto/request/TokenReissueRequest.java new file mode 100644 index 00000000..2fa40beb --- /dev/null +++ b/backend/src/main/java/kr/touroot/authentication/dto/request/TokenReissueRequest.java @@ -0,0 +1,10 @@ +package kr.touroot.authentication.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record TokenReissueRequest( + @Schema(description = "로그인 시 발급받은 리프레시 토큰") + @NotBlank(message = "리프레시 토큰은 비어있을 수 없습니다.") String refreshToken +) { +} diff --git a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java index fe22ab76..0a4013a8 100644 --- a/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/LoginResponse.java @@ -6,21 +6,21 @@ @Builder public record LoginResponse( - @Schema(description = "로그인한 유저의 PK", example = "1") - Long memberId, - @Schema(description = "로그인된 유저의 닉네임", example = "리비") - String nickname, + @Schema(description = "로그인한 유저의 PK", example = "1") Long memberId, + @Schema(description = "로그인된 유저의 닉네임", example = "리비") String nickname, @Schema(description = "로그인된 유저의 프로필 이미지 경로", example = "https://dev.touroot.kr/profile-image-ex.png") String profileImageUrl, - @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") - String accessToken) { + @Schema(description = "투룻 서비스 인가용 accessToken", example = "accessTokenValue") String accessToken, + @Schema(description = "투룻 서비스 refreshToken", example = "refreshTokenValue") String refreshToken +) { - public static LoginResponse of(Member member, String accessToken) { + public static LoginResponse of(Member member, TokenResponse tokenResponse) { return LoginResponse.builder() .memberId(member.getId()) .nickname(member.getNickname()) .profileImageUrl(member.getProfileImageUrl()) - .accessToken(accessToken) + .accessToken(tokenResponse.accessToken()) + .refreshToken(tokenResponse.refreshToken()) .build(); } } diff --git a/backend/src/main/java/kr/touroot/authentication/dto/response/TokenResponse.java b/backend/src/main/java/kr/touroot/authentication/dto/response/TokenResponse.java new file mode 100644 index 00000000..beb98b14 --- /dev/null +++ b/backend/src/main/java/kr/touroot/authentication/dto/response/TokenResponse.java @@ -0,0 +1,4 @@ +package kr.touroot.authentication.dto.response; + +public record TokenResponse(String accessToken, String refreshToken) { +} diff --git a/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java b/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java index 56521101..ab084857 100644 --- a/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/kr/touroot/authentication/infrastructure/JwtTokenProvider.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; import java.util.Date; +import kr.touroot.authentication.dto.response.TokenResponse; import kr.touroot.global.exception.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -14,20 +15,32 @@ public class JwtTokenProvider { private static final String MEMBER_ID_KEY = "id"; - private final String secretKey; - private final long validityInMilliseconds; + private final String accessSecretKey; + private final String refreshSecretKey; + private final long accessExpiration; + private final long refreshExpiration; public JwtTokenProvider( - @Value("${security.jwt.token.secret-key}") String secretKey, - @Value("${security.jwt.token.expire-length}") long validityInMilliseconds + @Value("${security.jwt.token.secret-key}") String accessSecretKey, + @Value("${security.jwt.refresh.secret-key}") String refreshSecretKey, + @Value("${security.jwt.token.expire-length}") long accessExpiration, + @Value("${security.jwt.refresh.expire-length}") long refreshExpiration ) { - this.secretKey = secretKey; - this.validityInMilliseconds = validityInMilliseconds; + this.accessSecretKey = accessSecretKey; + this.accessExpiration = accessExpiration; + this.refreshSecretKey = refreshSecretKey; + this.refreshExpiration = refreshExpiration; } - public String createToken(Long memberId) { + public TokenResponse createToken(Long memberId) { + String accessToken = createToken(memberId, accessSecretKey, accessExpiration); + String refreshToken = createToken(memberId, refreshSecretKey, refreshExpiration); + return new TokenResponse(accessToken, refreshToken); + } + + private String createToken(Long memberId, String secretKey, long expiration) { Date now = new Date(); - Date validity = new Date(now.getTime() + validityInMilliseconds); + Date validity = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(memberId.toString()) @@ -37,7 +50,15 @@ public String createToken(Long memberId) { .compact(); } - public String decode(String token) { + public String decodeAccessToken(String token) { + return decode(token, accessSecretKey); + } + + public String decodeRefreshToken(String token) { + return decode(token, refreshSecretKey); + } + + private String decode(String token, String secretKey) { try { return Jwts.parserBuilder() .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) diff --git a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java index 53f7c6f2..acbd7d6c 100644 --- a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java @@ -3,6 +3,7 @@ import kr.touroot.authentication.dto.request.LoginRequest; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import kr.touroot.authentication.dto.request.TokenReissueRequest; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.dto.response.OauthUserInformationResponse; import kr.touroot.authentication.infrastructure.JwtTokenProvider; @@ -43,4 +44,12 @@ public LoginResponse login(LoginRequest request) { return LoginResponse.of(member, tokenProvider.createToken(member.getId())); } + + public LoginResponse reissueToken(TokenReissueRequest request) { + String memberId = tokenProvider.decodeRefreshToken(request.refreshToken()); + Member member = memberRepository.findById(Long.valueOf(memberId)) + .orElseThrow(() -> new BadRequestException("존재하지 않는 사용자입니다.")); + + return LoginResponse.of(member, tokenProvider.createToken(member.getId())); + } } diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index f47c1f54..239053d2 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -56,7 +56,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse token = token.split("Bearer|bearer")[1]; try { - String memberId = tokenProvider.decode(token); + String memberId = tokenProvider.decodeAccessToken(token); request.setAttribute(MEMBER_ID_ATTRIBUTE, memberId); filterChain.doFilter(request, response); } catch (Exception e) { diff --git a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java index a31df532..3873cef1 100644 --- a/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/kr/touroot/global/exception/GlobalExceptionHandler.java @@ -79,4 +79,13 @@ public ResponseEntity handleS3UploadException(S3UploadExcepti return ResponseEntity.badRequest() .body(data); } + + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity handleUnauthorized(UnauthorizedException exception) { + log.warn("UNAUTHORIZED_EXCEPTION :: message = {}", exception.getMessage()); + + ExceptionResponse data = new ExceptionResponse(exception.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(data); + } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index cbc56cd3..cffb9098 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -16,11 +16,15 @@ cloud: temporary-storage-path: temporary/ image-storage-path: images/ --- +# local profile security: jwt: token: secret-key: ENC(SNwFG2NQDZkmIK3nNoZFdwQ0ZxKuoe+qcw10ljdW941YEx/Qky9PEEl+wvAN9S1KR26D3a83SnU=) - expire-length: 1800000 + expire-length: 1800000 # 30 min + refresh: + secret-key: ENC(tneEW6IKq9XuDoxAoKvBEVER4xjLHCycWXMa+Rnzb700ndTnrkJ2mOtBPP5hEIJLRNgj5MLIhYs=) + expire-length: 1209600000 # 14 days spring: config: activate: @@ -42,11 +46,15 @@ spring: ddl-auto: create-drop defer-datasource-initialization: true --- +# dev profile security: jwt: token: secret-key: ENC(L36WWjoZtP2nHHkqxDGlYLsMHMp+EBL2Fnl+X2de2KHk+PIfViyVM7rCYcbcFpo7yB4MaP++atU=) - expire-length: 1800000 + expire-length: 1800000 # 30 min + refresh: + secret-key: ENC(cDstTL4/ajLm3NohJwMR9vEBsIZeD9Vt+jE1obdwL8Q6gMnWvY3N+bmNsC9N0csaa6AaYIZLbFM=) + expire-length: 1209600000 # 14 days server: ssl: key-store-type: PKCS12 diff --git a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java index e793c929..df48c85c 100644 --- a/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/kr/touroot/authentication/controller/LoginControllerTest.java @@ -1,52 +1,85 @@ package kr.touroot.authentication.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.fasterxml.jackson.databind.ObjectMapper; -import kr.touroot.authentication.dto.response.LoginResponse; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import kr.touroot.authentication.dto.request.TokenReissueRequest; +import kr.touroot.authentication.dto.response.TokenResponse; +import kr.touroot.authentication.fixture.OauthUserInformationFixture; +import kr.touroot.authentication.helper.LoginTestHelper; import kr.touroot.authentication.infrastructure.JwtTokenProvider; -import kr.touroot.authentication.service.LoginService; -import kr.touroot.global.auth.JwtAuthFilter; +import kr.touroot.authentication.infrastructure.KakaoOauthProvider; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.member.domain.Member; +import kr.touroot.utils.DatabaseCleaner; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; +import org.springframework.boot.test.web.server.LocalServerPort; -@WebMvcTest(LoginController.class) +@AcceptanceTest class LoginControllerTest { @Autowired - private MockMvc mockMvc; + private JwtTokenProvider jwtTokenProvider; @Autowired - private ObjectMapper objectMapper; + private DatabaseCleaner databaseCleaner; @Autowired - private JwtAuthFilter jwtAuthFilter; - @MockBean - private LoginService loginService; - @MockBean - private JpaMetamodelMappingContext jpaMetamodelMappingContext; + private LoginTestHelper testHelper; @MockBean - private JwtTokenProvider jwtTokenProvider; + private KakaoOauthProvider oauthProvider; + @LocalServerPort + private int port; + + @BeforeEach + void setUp() { + RestAssured.port = port; + databaseCleaner.executeTruncate(); + } @DisplayName("카카오 로그인 요청을 처리할 수 있다") @Test void loginTest() throws Exception { - LoginResponse loginResponse = new LoginResponse(1L, "리비", "img-url", "test-access-token"); - when(loginService.login(any(String.class), any(String.class))).thenReturn(loginResponse); - - mockMvc.perform(post("/api/v1/login/oauth/kakao") - .param("code", "test-authorization-code") - .param("redirectUri", "https://touroot.kr/oauth")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(objectMapper.writeValueAsString(loginResponse))); + when(oauthProvider.getUserInformation(any(String.class), any(String.class))) + .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); + + RestAssured.given().log().all() + .queryParam("code", "test") + .queryParam("redirectUri", "https://test") + .when().log().all() + .post("/api/v1/login/oauth/kakao") + .then().log().all() + .statusCode(200) + .body("memberId", is(1)); + } + + @DisplayName("리프레시 토큰으로 재로그인을 할 수 있다.") + @Test + void reissueToken() { + // given + Member member = testHelper.initMemberTestData(); + TokenResponse tokenResponse = jwtTokenProvider.createToken(member.getId()); + TokenReissueRequest tokenReissueRequest = new TokenReissueRequest(tokenResponse.refreshToken()); + + // when + String accessToken = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(tokenReissueRequest) + .when().log().all() + .post("/api/v1/login/reissue-token") + .then().log().all() + .statusCode(200) + .extract() + .jsonPath().get("accessToken"); + String actual = jwtTokenProvider.decodeAccessToken(accessToken); + + // then + assertThat(actual).isEqualTo("1"); } } diff --git a/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java b/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java new file mode 100644 index 00000000..46e1da67 --- /dev/null +++ b/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java @@ -0,0 +1,19 @@ +package kr.touroot.authentication.helper; + +import kr.touroot.authentication.fixture.MemberFixture; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class LoginTestHelper { + + private final MemberRepository memberRepository; + + public Member initMemberTestData() { + Member member = MemberFixture.MEMBER_DEFAULT.getMember(); + return memberRepository.save(member); + } +} diff --git a/backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java b/backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java index 6f267dd7..897dde98 100644 --- a/backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java +++ b/backend/src/test/java/kr/touroot/authentication/infrastructure/JwtTokenProviderTest.java @@ -9,15 +9,21 @@ class JwtTokenProviderTest { private static final String TEST_SECRET_KEY = "test-secret-key-clover-leegun-naknak-alpaka-libi"; + private static final String TEST_REFRESH_KEY = "test-secret-key-clover-leegun-naknak-alpaka-libi"; private static final int EXPIRATION_TIME_30_MIN = 1800000; - private static final int EXPIRATION_TIME_0 = 0; + private static final int EXPIRATION_TIME_14_DAYS = 1209600000; private JwtTokenProvider jwtTokenProvider; @DisplayName("멤버를 통해 토큰을 만들 수 있다") @Test void createTokenTest() { - jwtTokenProvider = new JwtTokenProvider(TEST_SECRET_KEY, EXPIRATION_TIME_30_MIN); + jwtTokenProvider = new JwtTokenProvider( + TEST_SECRET_KEY, + TEST_REFRESH_KEY, + EXPIRATION_TIME_30_MIN, + EXPIRATION_TIME_14_DAYS + ); assertThat(jwtTokenProvider.createToken(1L)) .isNotNull(); diff --git a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java index 0ad38ede..8595d973 100644 --- a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java +++ b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import java.util.Optional; +import kr.touroot.authentication.dto.response.TokenResponse; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.fixture.OauthUserInformationFixture; import kr.touroot.authentication.infrastructure.JwtTokenProvider; @@ -35,38 +36,58 @@ class LoginServiceTest { @Mock private KakaoOauthProvider kakaoOauthProvider; @Mock - JwtTokenProvider jwtTokenProvider; + private JwtTokenProvider jwtTokenProvider; @DisplayName("투룻 회원가입이 되어 있는 회원의 카카오 소셜 로그인을 처리할 수 있다") @Test void existUserKakaoSocialLoginTest() { // given + String accessToken = "aaa"; + String refreshToken = "bbb"; + when(kakaoOauthProvider.getUserInformation(any(String.class), any(String.class))) .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); when(memberRepository.findByKakaoId(any(Long.class))) .thenReturn(Optional.of(MEMBER_KAKAO.getMember())); + when(jwtTokenProvider.createToken(MEMBER_KAKAO.getMember().getId())) + .thenReturn(new TokenResponse(accessToken, refreshToken)); + LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( - LoginResponse.of(MEMBER_KAKAO.getMember(), response.accessToken())); + LoginResponse.of( + MEMBER_KAKAO.getMember(), + new TokenResponse(response.accessToken(), response.refreshToken()) + ) + ); } @DisplayName("투룻 회원가입이 되어 있지 않은 회원은 소셜 로그인 과정에서 회원가입 후 로그인 된다") @Test void nonExistUserKakaoSocialLoginTest() { // given + String accessToken = "aaa"; + String refreshToken = "bbb"; + when(kakaoOauthProvider.getUserInformation(any(String.class), any(String.class))) .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); when(memberRepository.findByKakaoId(any(Long.class))) .thenReturn(Optional.empty()); when(memberRepository.save(any(Member.class))) .thenReturn(MEMBER_KAKAO.getMember()); + when(jwtTokenProvider.createToken(MEMBER_KAKAO.getMember().getId())) + .thenReturn(new TokenResponse(accessToken, refreshToken)); + LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( - LoginResponse.of(MEMBER_KAKAO.getMember(), response.accessToken())); + LoginResponse.of( + MEMBER_KAKAO.getMember(), + new TokenResponse(response.accessToken(), response.refreshToken()) + ) + ); verify(memberRepository, times(1)).save(any(Member.class)); } } diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index 7beaa31b..da1b3380 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -50,7 +50,8 @@ void setUp() { databaseCleaner.executeTruncate(); member = travelogueTestHelper.initKakaoMemberTestData(); - accessToken = jwtTokenProvider.createToken(member.getId()); + accessToken = jwtTokenProvider.createToken(member.getId()) + .accessToken(); } @DisplayName("마이 페이지 컨트롤러는 내 여행기 조회 요청이 들어오면 로그인한 사용자의 여행기를 조회한다.") diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index b28ec55c..688e9edf 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -44,8 +44,11 @@ class TravelogueControllerTest { private final JwtTokenProvider jwtTokenProvider; @MockBean private final AwsS3Provider s3Provider; + @LocalServerPort private int port; + private Member member; + private String accessToken; @Autowired public TravelogueControllerTest( @@ -67,6 +70,10 @@ void setUp() { RestAssured.port = port; databaseCleaner.executeTruncate(); + + member = testHelper.initKakaoMemberTestData(); + accessToken = jwtTokenProvider.createToken(member.getId()) + .accessToken(); } @DisplayName("여행기를 작성한다.") @@ -79,8 +86,6 @@ void createTravelogue() { List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); List days = TravelogueRequestFixture.getTravelogueDayRequests(places); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); - Member member = testHelper.initKakaoMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() .contentType(ContentType.JSON) @@ -103,7 +108,8 @@ void createTravelogueWithOver10PhotosEachPlaces() throws JsonProcessingException List days = TravelogueRequestFixture.getTravelogueDayRequests(places); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); Member member = testHelper.initKakaoMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); + String accessToken = jwtTokenProvider.createToken(member.getId()) + .accessToken(); ExceptionResponse response = new ExceptionResponse("여행기 장소 사진은 최대 10개입니다."); @@ -124,8 +130,6 @@ void createTravelogueWithNoDays() throws JsonProcessingException { .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(List.of()); - Member member = testHelper.initKakaoMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); ExceptionResponse response = new ExceptionResponse("여행기 일자는 최소 1일은 포함되어야 합니다."); @@ -147,8 +151,6 @@ void createTravelogueWithNoPlacesDay() throws JsonProcessingException { List days = TravelogueRequestFixture.getTravelogueDayRequests(List.of()); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); - Member member = testHelper.initKakaoMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); ExceptionResponse response = new ExceptionResponse("여행기 장소는 최소 한 곳은 포함되어야 합니다."); @@ -181,7 +183,7 @@ void createTravelogueWithNotLoginThrowException() { @DisplayName("여행기를 상세 조회한다.") @Test void findTravelogue() throws JsonProcessingException { - testHelper.initTravelogueTestData(); + testHelper.initTravelogueTestData(member); TravelogueResponse response = TravelogueResponseFixture.getTravelogueResponse(); RestAssured.given().log().all() @@ -220,9 +222,7 @@ void findNotExistTravelogueThrowException() { @DisplayName("여행기를 삭제한다.") @Test void deleteTravelogue() { - Member member = testHelper.initKakaoMemberTestData(); testHelper.initTravelogueTestData(member); - String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) @@ -234,9 +234,6 @@ void deleteTravelogue() { @DisplayName("존재하지 않는 여행기 삭제시 400를 응답한다.") @Test void deleteTravelogueWithNonExist() { - Member member = testHelper.initKakaoMemberTestData(); - String accessToken = jwtTokenProvider.createToken(member.getId()); - RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .when().delete("/api/v1/travelogues/1") @@ -250,7 +247,8 @@ void deleteTravelogueWithNonExist() { void deleteTravelogueWithNotAuthor() { Travelogue travelogue = testHelper.initTravelogueTestData(); Member notAuthor = testHelper.initKakaoMemberTestData(); - String accessToken = jwtTokenProvider.createToken(notAuthor.getId()); + String accessToken = jwtTokenProvider.createToken(notAuthor.getId()) + .accessToken(); RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index 95c18ba5..69a3c708 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -57,7 +57,7 @@ void setUp() { databaseCleaner.executeTruncate(); member = testHelper.initMemberTestData(); - accessToken = jwtTokenProvider.createToken(member.getId()); + accessToken = jwtTokenProvider.createToken(member.getId()).accessToken(); } @DisplayName("여행 계획 컨트롤러는 생성 요청이 들어올 때 200을 응답한다.") @@ -159,7 +159,8 @@ void readTravelPlanWithNotAuthor() { // given long id = testHelper.initTravelPlanTestData(member).getId(); Member notAuthor = testHelper.initMemberTestData(); - String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()) + .accessToken(); // when & then RestAssured.given().log().all() @@ -177,7 +178,6 @@ void readTravelPlanWithNotAuthor() { void readTravelPlanByShareKey() { // given TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); - String accessToken = jwtTokenProvider.createToken(member.getId()); // when & then RestAssured.given().log().all() @@ -196,7 +196,8 @@ void readTravelPlanByShareKeyFromNoAuthor() { // given TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); Member notAuthor = testHelper.initMemberTestData(); - String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()) + .accessToken(); // when & then RestAssured.given().log().all() @@ -230,8 +231,7 @@ void readTravelPlanByNotLoginUser() { @Test void readTravelPlanByInvalidShareKey() { // given - TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); - String accessToken = jwtTokenProvider.createToken(member.getId()); + testHelper.initTravelPlanTestData(member); // when & then RestAssured.given().log().all() @@ -248,7 +248,6 @@ void readTravelPlanByInvalidShareKey() { @Test void deleteTravelPlan() { long id = testHelper.initTravelPlanTestData(member).getId(); - String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) @@ -261,7 +260,6 @@ void deleteTravelPlan() { @Test void deleteTravelPlanWithNonExist() { long id = 1L; - String accessToken = jwtTokenProvider.createToken(member.getId()); RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) @@ -276,7 +274,7 @@ void deleteTravelPlanWithNonExist() { void deleteTravelPlanWithNotAuthor() { long id = testHelper.initTravelPlanTestData(member).getId(); Member notAuthor = testHelper.initMemberTestData(); - String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()).accessToken(); RestAssured.given().log().all() .header(HttpHeaders.AUTHORIZATION, "Bearer " + notAuthorAccessToken) diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index 644a4a20..0d119c92 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -8,7 +8,10 @@ security: jwt: token: secret-key: test-TADG67STFSDAGSDFSG4567UTKJHFHSDFGSR231DF - expire-length: 1800000 + expire-length: 1800000 # 30 min + refresh: + secret-key: test-TADG67STFSDAGSDFSG4567UTKJHFHSDFGSR231DF + expire-length: 1209600000 # 14 days cloud: aws: s3: From 4fd2ed416457f2ff5d6e5ca808efc4119ca15e66 Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:45:05 +0900 Subject: [PATCH 094/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=91=EC=84=B1/=EC=A1=B0=ED=9A=8C=20=EC=8B=9C?= =?UTF-8?q?=20=ED=83=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20(#305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 작성 시 태그 추가 구현 * test: 여행기 조회 시 태그 추가 테스트 작성 * feat: 메인 페이지 조회 시 Tag 추가 * refactor: TravelogueFacadeService/TagService 메서드 정리 * refactor: Tag가 없을 시 Null이 아닌 빈 리스트로 처리하게 변경 * test: 메인 페이지 조회 컨트롤러 테스트 통합 * refactor: 사용하지 않는 method 제거 --- .../tag/{ => controller}/TagController.java | 2 +- .../java/kr/touroot/tag/dto/TagResponse.java | 4 +- .../travelogue/domain/TravelogueTag.java | 37 +++++++++++++++++ .../dto/request/TravelogueRequest.java | 4 ++ .../dto/response/TravelogueResponse.java | 6 ++- .../response/TravelogueSimpleResponse.java | 10 ++++- .../repository/TravelogueTagRepository.java | 11 +++++ .../service/TravelogueFacadeService.java | 33 ++++++++++----- .../service/TravelogueTagService.java | 40 +++++++++++++++++++ .../kr/touroot/tag/fixture/TagFixture.java | 5 +++ .../controller/TravelogueControllerTest.java | 40 ++++++++++++++++++- .../fixture/TravelogueRequestFixture.java | 16 +++++++- .../fixture/TravelogueResponseFixture.java | 35 ++++++++++++++-- .../helper/TravelogueTestHelper.java | 33 ++++++++++++++- .../service/TravelogueFacadeServiceTest.java | 3 ++ 15 files changed, 255 insertions(+), 24 deletions(-) rename backend/src/main/java/kr/touroot/tag/{ => controller}/TagController.java (98%) create mode 100644 backend/src/main/java/kr/touroot/travelogue/domain/TravelogueTag.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java diff --git a/backend/src/main/java/kr/touroot/tag/TagController.java b/backend/src/main/java/kr/touroot/tag/controller/TagController.java similarity index 98% rename from backend/src/main/java/kr/touroot/tag/TagController.java rename to backend/src/main/java/kr/touroot/tag/controller/TagController.java index a2d925f3..3dffd2e6 100644 --- a/backend/src/main/java/kr/touroot/tag/TagController.java +++ b/backend/src/main/java/kr/touroot/tag/controller/TagController.java @@ -1,4 +1,4 @@ -package kr.touroot.tag; +package kr.touroot.tag.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java b/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java index c0fde1b5..ad44e28d 100644 --- a/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java +++ b/backend/src/main/java/kr/touroot/tag/dto/TagResponse.java @@ -6,8 +6,8 @@ @Builder public record TagResponse( - @Schema(description = "생성된 태그의 id") Long id, - @Schema(description = "생성된 태그의 내용") String tag + @Schema(description = "생성된 태그의 id", example = "1") Long id, + @Schema(description = "생성된 태그의 내용", example = "신나는 강아지 산책") String tag ) { public static TagResponse from(Tag tag) { diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueTag.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueTag.java new file mode 100644 index 00000000..ac289cf6 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueTag.java @@ -0,0 +1,37 @@ +package kr.touroot.travelogue.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import kr.touroot.tag.domain.Tag; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class TravelogueTag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "TRAVELOGUE_ID", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Travelogue travelogue; + + @JoinColumn(name = "TAG_ID", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Tag tag; + + public TravelogueTag(Travelogue travelogue, Tag tag) { + this(null, travelogue, tag); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java index 08a2cf52..8e896c09 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueRequest.java @@ -17,6 +17,10 @@ public record TravelogueRequest( @Schema(description = "여행기 썸네일", example = "S3 이미지 URL") @NotNull(message = "여행기 썸네일은 비어있을 수 없습니다.") String thumbnail, + @Schema(description = "선택된 여행기 태그의 id 목록", example = "[1, 2, 3]") + @NotNull(message = "여행기 태그 필드는 비어있을 수 없습니다.") + @Size(max = 5, message = "여행기 태그는 최대 5개까지 입력할 수 있습니다.") + List tags, @Schema(description = "여행기 일자 목록") @NotNull(message = "여행기 일자 목록은 비어있을 수 없습니다.") @Size(message = "여행기 일자는 최소 1일은 포함되어야 합니다.", min = 1) diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index feb71e2a..5f207912 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -3,6 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; import java.util.List; +import kr.touroot.tag.dto.TagResponse; import kr.touroot.travelogue.domain.Travelogue; import lombok.Builder; @@ -22,11 +23,13 @@ public record TravelogueResponse( String thumbnail, @Schema(description = "작성 날짜") LocalDate createdAt, + @Schema(description = "여행기 태그") + List tags, @Schema(description = "여행기 일자 목록") List days ) { - public static TravelogueResponse of(Travelogue travelogue, List days) { + public static TravelogueResponse of(Travelogue travelogue, List days, List tags) { return TravelogueResponse.builder() .id(travelogue.getId()) .createdAt(travelogue.getCreatedAt().toLocalDate()) @@ -36,6 +39,7 @@ public static TravelogueResponse of(Travelogue travelogue, List tags +) { - public static TravelogueSimpleResponse from(Travelogue travelogue) { + public static TravelogueSimpleResponse of(Travelogue travelogue, List tags) { return TravelogueSimpleResponse.builder() .id(travelogue.getId()) .title(travelogue.getTitle()) .thumbnail(travelogue.getThumbnail()) .authorNickname(travelogue.getAuthorNickname()) .authorProfileUrl(travelogue.getAuthorProfileImageUrl()) + .tags(tags) .build(); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java new file mode 100644 index 00000000..2f3f6fb7 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java @@ -0,0 +1,11 @@ +package kr.touroot.travelogue.repository; + +import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TravelogueTagRepository extends JpaRepository { + + List findAllByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index b9dccb63..2afba850 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -6,6 +6,7 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.member.domain.Member; import kr.touroot.member.service.MemberService; +import kr.touroot.tag.dto.TagResponse; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePhoto; @@ -33,14 +34,15 @@ public class TravelogueFacadeService { private final TravelogueDayService travelogueDayService; private final TraveloguePlaceService traveloguePlaceService; private final TraveloguePhotoService traveloguePhotoService; + private final TravelogueTagService travelogueTagService; private final MemberService memberService; @Transactional public TravelogueResponse createTravelogue(MemberAuth member, TravelogueRequest request) { Member author = memberService.getById(member.memberId()); Travelogue travelogue = travelogueService.createTravelogue(author, request); - - return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue)); + List tags = travelogueTagService.createTravelogueTags(travelogue, request.tags()); + return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue), tags); } private List createDays(List requests, Travelogue travelogue) { @@ -72,17 +74,12 @@ private List createPhotos(List requests, Travelo @Transactional(readOnly = true) public TravelogueResponse findTravelogueById(Long id) { Travelogue travelogue = travelogueService.getTravelogueById(id); - - return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue)); + return getTravelogueResponse(travelogue); } - @Transactional(readOnly = true) - public Page findSimpleTravelogues(final Pageable pageable) { - Page travelogues = travelogueService.findAll(pageable); - - return new PageImpl<>(travelogues.stream() - .map(TravelogueSimpleResponse::from) - .toList()); + private TravelogueResponse getTravelogueResponse(Travelogue travelogue) { + List tagResponses = travelogueTagService.readTagByTravelogue(travelogue); + return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses); } private List findDaysOfTravelogue(Travelogue travelogue) { @@ -107,6 +104,20 @@ private List findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) { return traveloguePhotoService.findPhotoUrlsByPlace(place); } + @Transactional(readOnly = true) + public Page findSimpleTravelogues(final Pageable pageable) { + Page travelogues = travelogueService.findAll(pageable); + + return new PageImpl<>(travelogues.stream() + .map(this::getTravelogueSimpleResponse) + .toList()); + } + + private TravelogueSimpleResponse getTravelogueSimpleResponse(Travelogue travelogue) { + List tagResponses = travelogueTagService.readTagByTravelogue(travelogue); + return TravelogueSimpleResponse.of(travelogue, tagResponses); + } + @Transactional public void deleteTravelogueById(Long id, MemberAuth member) { Member author = memberService.getById(member.memberId()); diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java new file mode 100644 index 00000000..901c6d8a --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java @@ -0,0 +1,40 @@ +package kr.touroot.travelogue.service; + +import java.util.List; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.tag.domain.Tag; +import kr.touroot.tag.dto.TagResponse; +import kr.touroot.tag.repository.TagRepository; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueTag; +import kr.touroot.travelogue.repository.TravelogueTagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class TravelogueTagService { + + private final TagRepository tagRepository; + private final TravelogueTagRepository travelogueTagRepository; + + public List createTravelogueTags(Travelogue travelogue, List tagIds) { + return tagIds.stream() + .map(id -> { + Tag tag = getTagById(id); + travelogueTagRepository.save(new TravelogueTag(travelogue, tag)); + return TagResponse.from(tag); + }).toList(); + } + + private Tag getTagById(Long id) { + return tagRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 태그입니다.")); + } + + public List readTagByTravelogue(Travelogue travelogue) { + return travelogueTagRepository.findAllByTravelogue(travelogue).stream() + .map(travelogueTag -> TagResponse.from(travelogueTag.getTag())) + .toList(); + } +} diff --git a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java index c2733ea6..19ec2a2e 100644 --- a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java +++ b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java @@ -2,6 +2,7 @@ import kr.touroot.tag.domain.Tag; import kr.touroot.tag.dto.TagCreateRequest; +import kr.touroot.tag.dto.TagResponse; public enum TagFixture { @@ -21,4 +22,8 @@ public Tag get() { public TagCreateRequest getCreateRequest() { return new TagCreateRequest(tag); } + + public TagResponse getResponse(Long id) { + return new TagResponse(id, tag); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 688e9edf..045c46e2 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -76,7 +76,7 @@ void setUp() { .accessToken(); } - @DisplayName("여행기를 작성한다.") + @DisplayName("태그가 없는 여행기를 작성한다.") @Test void createTravelogue() { Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) @@ -97,6 +97,29 @@ void createTravelogue() { .header("Location", "/api/v1/travelogues/1"); } + @DisplayName("태그가 있는 여행기를 작성한다.") + @Test + void createTravelogueWithTags() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + testHelper.initTagTestData(); + + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days, List.of(1L)); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().post("/api/v1/travelogues") + .then().log().all() + .statusCode(201) + .header("Location", "/api/v1/travelogues/1"); + } + @DisplayName("최대 업로드 가능 개수 이상의 사진을 포함한 여행기를 작성하면 예외가 발생한다.") @Test void createTravelogueWithOver10PhotosEachPlaces() throws JsonProcessingException { @@ -194,10 +217,25 @@ void findTravelogue() throws JsonProcessingException { .body(is(objectMapper.writeValueAsString(response))); } + @DisplayName("태그가 있는 여행기를 상세 조회한다.") + @Test + void findTravelogueWithTags() throws JsonProcessingException { + testHelper.initTravelogueTestDataWithTag(member); + TravelogueResponse response = TravelogueResponseFixture.getTravelogueResponseWithTag(); + + RestAssured.given().log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues/1") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + @DisplayName("메인 페이지 조회 시, 최신 작성 순으로 여행기를 조회한다.") @Test void findMainPageTravelogues() throws JsonProcessingException { testHelper.initTravelogueTestData(); + testHelper.initTravelogueTestDataWithTag(member); Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); RestAssured.given().log().all() diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index 03b4cdf1..0d749df0 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -15,7 +15,21 @@ private TravelogueRequestFixture() { } public static TravelogueRequest getTravelogueRequest(List days) { - return new TravelogueRequest("제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png", days); + return new TravelogueRequest( + "제주에 하영 옵서", + "https://dev.touroot.kr/temporary/jeju_thumbnail.png", + List.of(), + days + ); + } + + public static TravelogueRequest getTravelogueRequest(List days, List tags) { + return new TravelogueRequest( + "제주에 하영 옵서", + "https://dev.touroot.kr/temporary/jeju_thumbnail.png", + tags, + days + ); } public static List getTravelogueDayRequests(List places) { diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 961c43eb..c0ed5bf3 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import java.util.List; +import kr.touroot.tag.fixture.TagFixture; import kr.touroot.travelogue.dto.response.TravelogueDayResponse; import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; import kr.touroot.travelogue.dto.response.TraveloguePositionResponse; @@ -27,17 +28,43 @@ public static TravelogueResponse getTravelogueResponse() { .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) + .tags(List.of()) .build(); } - public static Page getTravelogueSimpleResponses() { - return new PageImpl<>(List.of(TravelogueSimpleResponse.builder() + public static TravelogueResponse getTravelogueResponseWithTag() { + return TravelogueResponse.builder() .id(1L) .title("제주에 하영 옵서") + .createdAt(LocalDate.now()) .authorNickname("리비") - .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") + .authorId(1L) + .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") - .build())); + .days(getTravelogueDayResponses()) + .tags(List.of(TagFixture.TAG.getResponse(1L))) + .build(); + } + + public static Page getTravelogueSimpleResponses() { + return new PageImpl<>(List.of( + TravelogueSimpleResponse.builder() + .id(2L) + .title("제주에 하영 옵서") + .authorNickname("리비") + .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") + .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") + .tags(List.of(TagFixture.TAG.getResponse(1L))) + .build(), + TravelogueSimpleResponse.builder() + .id(1L) + .title("제주에 하영 옵서") + .authorNickname("리비") + .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") + .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") + .tags(List.of()) + .build() + )); } public static List getTravelogueDayResponses() { diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 5470ed97..e6f8e9f4 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -12,14 +12,19 @@ import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.tag.domain.Tag; +import kr.touroot.tag.fixture.TagFixture; +import kr.touroot.tag.repository.TagRepository; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; +import kr.touroot.travelogue.domain.TravelogueTag; import kr.touroot.travelogue.repository.TravelogueDayRepository; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; import kr.touroot.travelogue.repository.TraveloguePlaceRepository; import kr.touroot.travelogue.repository.TravelogueRepository; +import kr.touroot.travelogue.repository.TravelogueTagRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -32,6 +37,8 @@ public class TravelogueTestHelper { private final TraveloguePlaceRepository traveloguePlaceRepository; private final TraveloguePhotoRepository traveloguePhotoRepository; private final MemberRepository memberRepository; + private final TagRepository tagRepository; + private final TravelogueTagRepository travelogueTagRepository; @Autowired public TravelogueTestHelper( @@ -40,7 +47,9 @@ public TravelogueTestHelper( TravelogueDayRepository travelogueDayRepository, TraveloguePlaceRepository traveloguePlaceRepository, TraveloguePhotoRepository traveloguePhotoRepository, - MemberRepository memberRepository + MemberRepository memberRepository, + TagRepository tagRepository, + TravelogueTagRepository travelogueTagRepository ) { this.placeRepository = placeRepository; this.travelogueRepository = travelogueRepository; @@ -48,6 +57,8 @@ public TravelogueTestHelper( this.traveloguePlaceRepository = traveloguePlaceRepository; this.traveloguePhotoRepository = traveloguePhotoRepository; this.memberRepository = memberRepository; + this.tagRepository = tagRepository; + this.travelogueTagRepository = travelogueTagRepository; } public Travelogue initTravelogueTestData() { @@ -65,6 +76,22 @@ public Travelogue initTravelogueTestData(Member author) { return travelogue; } + public Travelogue initTravelogueTestDataWithTag(Member author) { + Travelogue travelogue = persistTravelogue(author); + TravelogueDay day = persistTravelogueDay(travelogue); + Place position = persistPlace(); + TraveloguePlace place = persistTraveloguePlace(position, day); + persistTraveloguePhoto(place); + persisTravelogueTag(travelogue); + + return travelogue; + } + + private void persisTravelogueTag(Travelogue travelogue) { + Tag tag = initTagTestData(); + travelogueTagRepository.save(new TravelogueTag(travelogue, tag)); + } + public void initTravelogueTestDate(Member author) { Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); @@ -113,4 +140,8 @@ public Member initKakaoMemberTestData() { Member member = new Member(1L, "리비", "https://dev.touroot.kr/temporary/profile.png", LoginType.KAKAO); return memberRepository.save(member); } + + public Tag initTagTestData() { + return tagRepository.save(TagFixture.TAG.get()); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 59a9f488..8d931879 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -22,6 +22,7 @@ import kr.touroot.travelogue.helper.TravelogueTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +38,7 @@ TraveloguePhotoService.class, TravelogueDayService.class, TraveloguePlaceService.class, + TravelogueTagService.class, MemberService.class, TravelogueTestHelper.class, AwsS3Provider.class, @@ -100,6 +102,7 @@ void findTravelogueById() { .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } + @Disabled @DisplayName("메인 페이지에 표시할 여행기 목록을 조회한다.") @Test void findTravelogues() { From 0f36b698f25c0d5184a2c7238258fa40efdc3183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:08:04 +0900 Subject: [PATCH 095/108] =?UTF-8?q?[Feature]=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 멤버의 닉네임을 변경하는 기능 구현 * feat: 멤버의 닉네임을 변경하는 API 구현 * feat: CORS 메소드에 PATCH 추가 * style: Swagger 오타 수정 * feat: 프로필 수정 응답 dto 수정 * test: `@NullAndEmptySource` 사용해서 테스트 코드 리팩토링 --- .../kr/touroot/global/config/WebConfig.java | 2 +- .../member/controller/MyPageController.java | 29 +++++++ .../java/kr/touroot/member/domain/Member.java | 34 ++++++--- .../dto/request/ProfileUpdateRequest.java | 11 +++ .../touroot/member/service/MemberService.java | 12 +++ .../member/service/MyPageFacadeService.java | 6 ++ .../controller/MyPageControllerTest.java | 20 +++++ .../kr/touroot/member/domain/MemberTest.java | 75 ++++++++++--------- .../member/service/MemberServiceTest.java | 17 ++++- 9 files changed, 159 insertions(+), 47 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java diff --git a/backend/src/main/java/kr/touroot/global/config/WebConfig.java b/backend/src/main/java/kr/touroot/global/config/WebConfig.java index 2086cc05..18e1fd70 100644 --- a/backend/src/main/java/kr/touroot/global/config/WebConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/WebConfig.java @@ -23,6 +23,6 @@ public void addArgumentResolvers(List resolvers) public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000", "https://dev.touroot.kr") - .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS"); + .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS"); } } diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index 7ca42ac8..219c0439 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -7,11 +7,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.member.dto.MyTravelogueResponse; import kr.touroot.member.dto.ProfileResponse; +import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.member.service.MyPageFacadeService; import kr.touroot.travelplan.dto.response.TravelPlanResponse; import lombok.RequiredArgsConstructor; @@ -22,6 +24,8 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -98,4 +102,29 @@ public ResponseEntity> readTravelPlans( Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); return ResponseEntity.ok(data); } + + @Operation(summary = "나의 프로필 정보 수정") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "내 프로필 정보 수정에 성공했을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 값이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 프로필 정보 수정을 시도할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PatchMapping("/profile") + public ResponseEntity updateProfile( + @Valid @RequestBody ProfileUpdateRequest request, + @NotNull MemberAuth memberAuth + ) { + return ResponseEntity.ok(myPageFacadeService.updateProfile(request, memberAuth)); + } } diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index 038a7840..5859b974 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -13,6 +13,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -67,12 +68,22 @@ private void validate( Long kakaoId, String email, String password, String nickname, String profileImageUrl, LoginType loginType ) { validateByLoginType(kakaoId, email, password, loginType); - validateNotNull(nickname, profileImageUrl); - validateNotBlank(nickname, profileImageUrl); + validateNickname(nickname); + validateProfileImageUrl(profileImageUrl); validateNicknameLength(nickname); validateProfileImageUrl(profileImageUrl); } + private void validateNickname(String nickname) { + validateNicknameNotBlank(nickname); + validateNicknameLength(nickname); + } + + private void validateProfileImageUrl(String profileImageUrl) { + validateProfileImageUrlNotBlank(profileImageUrl); + validateProfileImageUrlForm(profileImageUrl); + } + private void validateByLoginType(Long kakaoId, String email, String password, LoginType loginType) { if (loginType.equals(LoginType.KAKAO) && kakaoId == null) { throw new BadRequestException("카카오 ID는 비어 있을 수 없습니다"); @@ -87,15 +98,15 @@ private void validateByLoginType(Long kakaoId, String email, String password, Lo } } - private void validateNotNull(String nickname, String profileImageUrl) { - if (nickname == null || profileImageUrl == null) { - throw new BadRequestException("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + private void validateNicknameNotBlank(String nickname) { + if (StringUtils.isBlank(nickname)) { + throw new BadRequestException("닉네임은 비어 있을 수 없습니다"); } } - private void validateNotBlank(String nickname, String profileImageUrl) { - if (nickname.isBlank() || profileImageUrl.isBlank()) { - throw new BadRequestException("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + private void validateProfileImageUrlNotBlank(String profileImageUrl) { + if (StringUtils.isBlank(profileImageUrl)) { + throw new BadRequestException("프로필 이미지는 비어 있을 수 없습니다"); } } @@ -107,11 +118,16 @@ private void validateNicknameLength(String nickname) { } } - private void validateProfileImageUrl(String profileImageUrl) { + private void validateProfileImageUrlForm(String profileImageUrl) { try { new URL(profileImageUrl).toURI(); } catch (Exception e) { throw new BadRequestException("이미지 url 형식이 잘못되었습니다"); } } + + public void changeNickname(String nickname) { + validateNickname(nickname); + this.nickname = nickname; + } } diff --git a/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java b/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java new file mode 100644 index 00000000..45926ed6 --- /dev/null +++ b/backend/src/main/java/kr/touroot/member/dto/request/ProfileUpdateRequest.java @@ -0,0 +1,11 @@ +package kr.touroot.member.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record ProfileUpdateRequest( + @Schema(description = "사용자 닉네임", example = "아기뚜리") + @NotBlank(message = "닉네임은 비어있을 수 없습니다.") + String nickname +) { +} diff --git a/backend/src/main/java/kr/touroot/member/service/MemberService.java b/backend/src/main/java/kr/touroot/member/service/MemberService.java index 89ba31dd..22e8e35e 100644 --- a/backend/src/main/java/kr/touroot/member/service/MemberService.java +++ b/backend/src/main/java/kr/touroot/member/service/MemberService.java @@ -1,12 +1,16 @@ package kr.touroot.member.service; import kr.touroot.authentication.infrastructure.PasswordEncryptor; +import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; +import kr.touroot.member.dto.ProfileResponse; import kr.touroot.member.dto.request.MemberRequest; +import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -44,4 +48,12 @@ private void validateNicknameDuplication(String nickname) { throw new BadRequestException("이미 사용 중인 닉네임입니다."); } } + + @Transactional + public ProfileResponse updateProfile(ProfileUpdateRequest request, MemberAuth memberAuth) { + Member member = getById(memberAuth.memberId()); + member.changeNickname(request.nickname()); + + return ProfileResponse.from(member); + } } diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java index b32c0fd2..ec9153b9 100644 --- a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -4,6 +4,7 @@ import kr.touroot.member.domain.Member; import kr.touroot.member.dto.MyTravelogueResponse; import kr.touroot.member.dto.ProfileResponse; +import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.service.TravelogueService; import kr.touroot.travelplan.domain.TravelPlan; @@ -44,4 +45,9 @@ public Page readTravelPlans(MemberAuth memberAuth, Pageable return travelPlans.map((travelPlanService::getTravelPlanResponse)); } + + @Transactional + public ProfileResponse updateProfile(ProfileUpdateRequest request, MemberAuth memberAuth) { + return memberService.updateProfile(request, memberAuth); + } } diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index da1b3380..86885f42 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -7,6 +7,7 @@ import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; +import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.travelogue.helper.TravelogueTestHelper; import kr.touroot.travelplan.helper.TravelPlanTestHelper; import kr.touroot.utils.DatabaseCleaner; @@ -91,4 +92,23 @@ void readTravelPlans() { .statusCode(200) .body("content.size()", is(2)); } + + @DisplayName("마이 페이지 컨트롤러는 내 프로필 수정 요청이 들어오면 로그인한 사용자의 프로필을 수정한다.") + @Test + void updateProfile() { + // given + String newNickname = "newNickname"; + ProfileUpdateRequest request = new ProfileUpdateRequest(newNickname); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().log().all() + .patch("/api/v1/member/me/profile") + .then().log().all() + .statusCode(200) + .body("nickname", is(newNickname)); + } } diff --git a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java index 6fd9c34c..26c1ca87 100644 --- a/backend/src/test/java/kr/touroot/member/domain/MemberTest.java +++ b/backend/src/test/java/kr/touroot/member/domain/MemberTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; @DisplayName("멤버") @@ -45,16 +46,6 @@ void createMemberWithBlankEmail(String blankEmail) { .hasMessage("이메일과 비밀번호는 비어 있을 수 없습니다."); } - @DisplayName("프로필 이미지 경로가 비어 있는 경우 멤버 생성 시 예외가 발생한다") - @ParameterizedTest - @ValueSource(strings = {"", " "}) - void createMemberWithBlankPassword(String blankPassword) { - assertThatThrownBy( - () -> new Member(VALID_EMAIL, blankPassword, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, DEFAULT)) - .isInstanceOf(BadRequestException.class) - .hasMessage("이메일과 비밀번호는 비어 있을 수 없습니다."); - } - @DisplayName("카카오 로그인 시, 카카오 아이디가 null일 때 멤버 생성 시 예외가 발생한다") @Test void createMemberWithKakaoIdNull() { @@ -63,38 +54,22 @@ void createMemberWithKakaoIdNull() { .hasMessage("카카오 ID는 비어 있을 수 없습니다"); } - @DisplayName("닉네임이 null인 경우 멤버 생성 시 예외가 발생한다") - @Test - void createMemberWithNicknameNull() { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, null, VALID_PROFILE_IMAGE_URL, KAKAO)) - .isInstanceOf(BadRequestException.class) - .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); - } - - @DisplayName("프로필 이미지 경로가 null일 경우 멤버 생성 시 예외가 발생한다") - @Test - void createMemberWithProfileImageUrlNull() { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, null, KAKAO)) - .isInstanceOf(BadRequestException.class) - .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); - } - - @DisplayName("닉네임이 비어 있는 경우 멤버 생성 시 예외가 발생한다") + @DisplayName("닉네임이 null이거나 비어 있는 경우 멤버 생성 시 예외가 발생한다") @ParameterizedTest - @ValueSource(strings = {"", " "}) - void createMemberWithBlankNickname(String blankNickname) { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, blankNickname, VALID_PROFILE_IMAGE_URL, KAKAO)) + @NullAndEmptySource() + void createMemberWithNullOrEmptyNickname(String nullOrEmptyNickname) { + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, nullOrEmptyNickname, VALID_PROFILE_IMAGE_URL, KAKAO)) .isInstanceOf(BadRequestException.class) - .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + .hasMessage("닉네임은 비어 있을 수 없습니다"); } - @DisplayName("프로필 이미지 경로가 비어 있는 경우 멤버 생성 시 예외가 발생한다") + @DisplayName("프로필 이미지 경로가 null이거나 비어 있는 경우 멤버 생성 시 예외가 발생한다") @ParameterizedTest - @ValueSource(strings = {"", " "}) - void createMemberWithProfileImageBlank(String blankUrl) { - assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, blankUrl, KAKAO)) + @NullAndEmptySource() + void createMemberWithNullOrEmptyProfileImage(String nullOrEmptyUrl) { + assertThatThrownBy(() -> new Member(VALID_SOCIAl_ID, VALID_NICKNAME, nullOrEmptyUrl, KAKAO)) .isInstanceOf(BadRequestException.class) - .hasMessage("닉네임, 프로필 이미지는 비어 있을 수 없습니다"); + .hasMessage("프로필 이미지는 비어 있을 수 없습니다"); } @DisplayName("닉네임의 길이가 범위를 벗어나면 멤버 생성 시 예외가 발생한다") @@ -114,4 +89,32 @@ void createMemberWithInvalidProfileImageUrl(String invalidProfileImageUrl) { .isInstanceOf(BadRequestException.class) .hasMessage("이미지 url 형식이 잘못되었습니다"); } + + @DisplayName("검증 규칙을 통과하는 닉네임 변경은 예외가 발생하지 않는다") + @Test + void changeNicknameWithValidData() { + Member member = new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO); + assertThatCode(() -> member.changeNickname(VALID_NICKNAME + "a")) + .doesNotThrowAnyException(); + } + + @DisplayName("null이나 비어있는 닉네임으로 변경 시 예외가 발생한다") + @ParameterizedTest + @NullAndEmptySource() + void changeNicknameWithOrEmpty(String nullOrEmptyNickname) { + Member member = new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO); + assertThatThrownBy(() -> member.changeNickname(nullOrEmptyNickname)) + .isInstanceOf(BadRequestException.class) + .hasMessage("닉네임은 비어 있을 수 없습니다"); + } + + @DisplayName("범위를 벗어난 길이의 닉네임으로 변경 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {"21-length-nicknameeee", "22-length-nicknameeeee"}) + void changeNicknameWithInvalidLength(String invalidLengthNickname) { + Member member = new Member(VALID_SOCIAl_ID, VALID_NICKNAME, VALID_PROFILE_IMAGE_URL, KAKAO); + assertThatThrownBy(() -> member.changeNickname(invalidLengthNickname)) + .isInstanceOf(BadRequestException.class) + .hasMessage("닉네임은 1자 이상, 20자 이하여야 합니다"); + } } diff --git a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java index 0347129b..ffb8b24c 100644 --- a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java +++ b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java @@ -8,9 +8,11 @@ import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.ServiceTest; +import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import kr.touroot.member.dto.request.MemberRequest; +import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.member.helper.MemberTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; @@ -91,4 +93,17 @@ void createMemberWithDuplicatedNickname() { .isInstanceOf(BadRequestException.class) .hasMessage("이미 사용 중인 닉네임입니다."); } -} \ No newline at end of file + + @DisplayName("멤버의 프로필을 업데이트 한다.") + @Test + void updateProfile() { + Member member = testHelper.persistMember(); + MemberAuth memberAuth = new MemberAuth(member.getId()); + ProfileUpdateRequest request = new ProfileUpdateRequest("newNickname"); + + memberService.updateProfile(request, memberAuth); + + assertThat(memberService.getById(member.getId()).getNickname()) + .isEqualTo("newNickname"); + } +} From c24d508d55446ad3984295d47dba4b2ea7755918 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Wed, 14 Aug 2024 15:23:49 +0900 Subject: [PATCH 096/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=9C=EB=AA=A9=20=EA=B8=B0=EC=A4=80=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 제목 키워드 기준 검색 기능 구현 * feat: 여행기 검색 기능을 위한 키워드 검증 예외 처리 * feat: QueryDSL 의존성 추가 * refactor: 예외 메시지 추출 로직 변경 * refactor: API 문서 설명 수정 * test: 여행기 제목 키워드 검색 기능 테스트 작성 * feat: 여행기 제목 키워드 기준 검색 기능 구현 * refactor: 추상화에 의존하도록 변경 * refactor: 필드 final 추가 * refactor: DTO 변환 과정 개선 * refactor: 필요 없어진 예외 처리 로직 제거 * refactor: 검색 키워드 request parameter DTO로 분리 * refactor: 검색 메소드 시그니처 리팩토링 * refactor: 여행기 조회 테스트 검증 대상 수정 * chore: 오타 수정 * refactor: pagination 관련 테스트 fixture 수정 * fix: conflict 해결 * refactor: 조회 쿼리에 정렬 및 페이지네이션 정보 추가 --- backend/build.gradle | 6 +++ .../touroot/global/config/QueryDslConfig.java | 21 ++++++++ .../controller/TravelogueController.java | 25 +++++++++ .../dto/request/TravelogueSearchRequest.java | 11 ++++ .../repository/TravelogueQueryRepository.java | 10 ++++ .../TravelogueQueryRepositoryImpl.java | 33 ++++++++++++ .../service/TravelogueFacadeService.java | 13 +++-- .../travelogue/service/TravelogueService.java | 6 +++ .../global/config/TestQueryDslConfig.java | 28 ++++++++++ .../controller/MyPageControllerTest.java | 4 +- .../controller/TravelogueControllerTest.java | 53 ++++++++++++++++++- .../fixture/TravelogueResponseFixture.java | 9 +++- .../helper/TravelogueTestHelper.java | 14 +++-- .../service/TravelogueFacadeServiceTest.java | 31 ++++++++--- .../service/TravelogueServiceTest.java | 22 +++++++- 15 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/global/config/QueryDslConfig.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java create mode 100644 backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java diff --git a/backend/build.gradle b/backend/build.gradle index 94668462..2ab14a0a 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -51,6 +51,12 @@ dependencies { // cache implementation 'org.springframework.boot:spring-boot-starter-cache' implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") + + // QueryDSL + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { diff --git a/backend/src/main/java/kr/touroot/global/config/QueryDslConfig.java b/backend/src/main/java/kr/touroot/global/config/QueryDslConfig.java new file mode 100644 index 00000000..93be678f --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/config/QueryDslConfig.java @@ -0,0 +1,21 @@ +package kr.touroot.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@RequiredArgsConstructor +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index 88030b9b..2e795bed 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -12,6 +12,7 @@ import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.service.TravelogueFacadeService; @@ -100,6 +101,30 @@ public ResponseEntity> findMainPageTravelogues( return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(pageable)); } + @Operation(summary = "여행기 검색") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "올바르지 않은 페이지네이션 옵션 또는 키워드로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @PageableAsQueryParam + @GetMapping("/search") + public ResponseEntity> findTraveloguesByKeyword( + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Direction.DESC) + Pageable pageable, + @Valid + TravelogueSearchRequest searchRequest + ) { + return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(pageable, searchRequest)); + } + @Operation(summary = "여행기 삭제") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java new file mode 100644 index 00000000..d393c200 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/dto/request/TravelogueSearchRequest.java @@ -0,0 +1,11 @@ +package kr.touroot.travelogue.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record TravelogueSearchRequest( + @NotBlank(message = "검색어는 2글자 이상이어야 합니다.") + @Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.") + String keyword +) { +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java new file mode 100644 index 00000000..febbb4a5 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java @@ -0,0 +1,10 @@ +package kr.touroot.travelogue.repository; + +import kr.touroot.travelogue.domain.Travelogue; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface TravelogueQueryRepository { + + Page findByTitleContaining(String keyword, Pageable pageable); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java new file mode 100644 index 00000000..3c940ef9 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java @@ -0,0 +1,33 @@ +package kr.touroot.travelogue.repository; + +import static kr.touroot.travelogue.domain.QTravelogue.travelogue; + +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class TravelogueQueryRepositoryImpl implements TravelogueQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Page findByTitleContaining(String keyword, Pageable pageable) { + List results = jpaQueryFactory.selectFrom(travelogue) + .where(Expressions.stringTemplate("replace({0}, ' ', '')", travelogue.title) + .containsIgnoreCase(keyword.replace(" ", ""))) + .orderBy(travelogue.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(results, pageable, results.size()); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index 2afba850..c5a91d85 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -15,13 +15,13 @@ import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; import kr.touroot.travelogue.dto.response.TravelogueDayResponse; import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -108,9 +108,14 @@ private List findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) { public Page findSimpleTravelogues(final Pageable pageable) { Page travelogues = travelogueService.findAll(pageable); - return new PageImpl<>(travelogues.stream() - .map(this::getTravelogueSimpleResponse) - .toList()); + return travelogues.map(this::getTravelogueSimpleResponse); + } + + @Transactional(readOnly = true) + public Page findSimpleTravelogues(Pageable pageable, TravelogueSearchRequest request) { + Page travelogues = travelogueService.findByKeyword(request.keyword(), pageable); + + return travelogues.map(this::getTravelogueSimpleResponse); } private TravelogueSimpleResponse getTravelogueSimpleResponse(Travelogue travelogue) { diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index e3701b64..300e269d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -6,6 +6,7 @@ import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.repository.TravelogueQueryRepository; import kr.touroot.travelogue.repository.TravelogueRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -18,6 +19,7 @@ public class TravelogueService { private final TravelogueRepository travelogueRepository; private final AwsS3Provider s3Provider; + private final TravelogueQueryRepository travelogueQueryRepository; public Travelogue createTravelogue(Member author, TravelogueRequest request) { String url = s3Provider.copyImageToPermanentStorage(request.thumbnail()); @@ -38,6 +40,10 @@ public Page findAllByMember(Member member, Pageable pageable) { return travelogueRepository.findAllByAuthor(member, pageable); } + public Page findByKeyword(String keyword, Pageable pageable) { + return travelogueQueryRepository.findByTitleContaining(keyword, pageable); + } + public void delete(Travelogue travelogue, Member author) { validateDeleteByAuthor(travelogue, author); travelogueRepository.delete(travelogue); diff --git a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java new file mode 100644 index 00000000..4fc14dac --- /dev/null +++ b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java @@ -0,0 +1,28 @@ +package kr.touroot.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import kr.touroot.travelogue.repository.TravelogueQueryRepository; +import kr.touroot.travelogue.repository.TravelogueQueryRepositoryImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@RequiredArgsConstructor +@TestConfiguration +public class TestQueryDslConfig { + + @PersistenceContext + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } + + @Bean + public TravelogueQueryRepository travelogueQueryRepository() { + return new TravelogueQueryRepositoryImpl(jpaQueryFactory()); + } +} diff --git a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java index 86885f42..4df72d07 100644 --- a/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java +++ b/backend/src/test/java/kr/touroot/member/controller/MyPageControllerTest.java @@ -59,8 +59,8 @@ void setUp() { @Test void readTravelogues() { // given - travelogueTestHelper.initTravelogueTestDate(member); - travelogueTestHelper.initTravelogueTestDate(member); + travelogueTestHelper.initTravelogueTestData(member); + travelogueTestHelper.initTravelogueTestData(member); travelogueTestHelper.initTravelogueTestData(); // when & then diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 045c46e2..6ffca5d1 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -27,6 +27,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -234,8 +237,7 @@ void findTravelogueWithTags() throws JsonProcessingException { @DisplayName("메인 페이지 조회 시, 최신 작성 순으로 여행기를 조회한다.") @Test void findMainPageTravelogues() throws JsonProcessingException { - testHelper.initTravelogueTestData(); - testHelper.initTravelogueTestDataWithTag(member); + testHelper.initAllTravelogueTestData(); Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); RestAssured.given().log().all() @@ -257,6 +259,53 @@ void findNotExistTravelogueThrowException() { .body("message", is("존재하지 않는 여행기입니다.")); } + @DisplayName("제목 키워드를 기준으로 여행기를 조회할 수 있다.") + @Test + void findTraveloguesByTitleKeyword() throws JsonProcessingException { + testHelper.initAllTravelogueTestData(); + Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); + + RestAssured.given().param("keyword", "제주") + .log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues/search") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(responses))); + } + + @DisplayName("제목 키워드는 2글자 이상이어야 한다.") + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " "}) + void findTraveloguesKeywordNotBlank(String keyword) { + testHelper.initTravelogueTestData(); + + RestAssured.given().param("keyword", keyword) + .log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues/search") + .then().log().all() + .statusCode(400).assertThat() + .body("message", is("검색어는 2글자 이상이어야 합니다.")); + } + + @DisplayName("제목 키워드는 중간 공백 상관 없이 검색되어야 한다.") + @ParameterizedTest + @ValueSource(strings = {"제 주", "제주 에하영옵 서"}) + void findTraveloguesKeywordWithMiddleBlank(String keyword) throws JsonProcessingException { + testHelper.initAllTravelogueTestData(); + Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); + + RestAssured.given().param("keyword", keyword) + .log().all() + .accept(ContentType.JSON) + .when().get("/api/v1/travelogues/search") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(responses))); + } + @DisplayName("여행기를 삭제한다.") @Test void deleteTravelogue() { diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index c0ed5bf3..1defa64a 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -10,6 +10,9 @@ import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.stereotype.Component; @Component @@ -47,7 +50,7 @@ public static TravelogueResponse getTravelogueResponseWithTag() { } public static Page getTravelogueSimpleResponses() { - return new PageImpl<>(List.of( + List responses = List.of( TravelogueSimpleResponse.builder() .id(2L) .title("제주에 하영 옵서") @@ -64,7 +67,9 @@ public static Page getTravelogueSimpleResponses() { .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .tags(List.of()) .build() - )); + ); + + return new PageImpl<>(responses, PageRequest.of(0, 5, Sort.by(Direction.DESC, "id")), responses.size()); } public static List getTravelogueDayResponses() { diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index e6f8e9f4..47028b70 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -61,6 +61,12 @@ public TravelogueTestHelper( this.travelogueTagRepository = travelogueTagRepository; } + public void initAllTravelogueTestData() { + Member author = persistMember(); + initTravelogueTestData(author); + initTravelogueTestDataWithTag(author); + } + public Travelogue initTravelogueTestData() { Member author = persistMember(); return initTravelogueTestData(author); @@ -92,14 +98,6 @@ private void persisTravelogueTag(Travelogue travelogue) { travelogueTagRepository.save(new TravelogueTag(travelogue, tag)); } - public void initTravelogueTestDate(Member author) { - Travelogue travelogue = persistTravelogue(author); - TravelogueDay day = persistTravelogueDay(travelogue); - Place position = persistPlace(); - TraveloguePlace place = persistTraveloguePlace(position, day); - persistTraveloguePhoto(place); - } - public Member persistMember() { Member author = MEMBER_KAKAO.getMember(); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 8d931879..b0e28929 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -8,6 +8,7 @@ import kr.touroot.authentication.infrastructure.PasswordEncryptor; import kr.touroot.global.ServiceTest; import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.config.TestQueryDslConfig; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; @@ -16,20 +17,21 @@ import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.fixture.TravelogueResponseFixture; import kr.touroot.travelogue.helper.TravelogueTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; @DisplayName("여행기 Facade 서비스") @Import(value = { @@ -42,7 +44,8 @@ MemberService.class, TravelogueTestHelper.class, AwsS3Provider.class, - PasswordEncryptor.class + PasswordEncryptor.class, + TestQueryDslConfig.class }) @ServiceTest class TravelogueFacadeServiceTest { @@ -102,15 +105,29 @@ void findTravelogueById() { .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } - @Disabled @DisplayName("메인 페이지에 표시할 여행기 목록을 조회한다.") @Test void findTravelogues() { - testHelper.initTravelogueTestData(); + testHelper.initAllTravelogueTestData(); + Page expect = TravelogueResponseFixture.getTravelogueSimpleResponses(); + + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); + Page result = service.findSimpleTravelogues(pageRequest); + + assertThat(result).containsAll(expect); + } + + @DisplayName("제목 키워드를 기반으로 여행기 목록을 조회한다.") + @Test + void findTraveloguesByKeyword() { + testHelper.initAllTravelogueTestData(); Page responses = TravelogueResponseFixture.getTravelogueSimpleResponses(); - assertThat(service.findSimpleTravelogues(Pageable.ofSize(5))) - .isEqualTo(responses); + TravelogueSearchRequest searchRequest = new TravelogueSearchRequest("제주"); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); + Page searchResults = service.findSimpleTravelogues(pageRequest, searchRequest); + + assertThat(searchResults).containsAll(responses); } @DisplayName("여행기를 ID를 기준으로 삭제한다.") diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index fc8260e0..0ceb7f84 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -8,6 +8,7 @@ import java.util.List; import kr.touroot.global.ServiceTest; +import kr.touroot.global.config.TestQueryDslConfig; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; @@ -31,7 +32,7 @@ import org.springframework.data.domain.Pageable; @DisplayName("여행기 서비스") -@Import(value = {TravelogueService.class, TravelogueTestHelper.class, AwsS3Provider.class}) +@Import(value = {TravelogueService.class, TravelogueTestHelper.class, AwsS3Provider.class, TestQueryDslConfig.class}) @ServiceTest class TravelogueServiceTest { @@ -97,6 +98,7 @@ void getTravelogueByNotExistsIdThrowException() { .hasMessage("존재하지 않는 여행기입니다."); } + @DisplayName("여행기를 전체 조회할 수 있다.") @Test void findAll() { @@ -106,6 +108,24 @@ void findAll() { .hasSize(1); } + @DisplayName("여행기를 전체 조회할 수 있다.") + @Test + void findByKeyword() { + testHelper.initTravelogueTestData(); + + assertThat(travelogueService.findByKeyword("제주", Pageable.ofSize(BASIC_PAGE_SIZE))) + .hasSize(1); + } + + @DisplayName("존재하지 않는 키워드로 여행기를 조회하면 빈 페이지가 반환된다.") + @Test + void findByKeywordWithNotExistKeyword() { + testHelper.initTravelogueTestData(); + + assertThat(travelogueService.findByKeyword("서울", Pageable.ofSize(BASIC_PAGE_SIZE))) + .isEmpty(); + } + @DisplayName("여행기를 삭제할 수 있다.") @Test void deleteTravelogueById() { From dcecd7526cf65ca1f2dbdb4ce91cf47fc0f73c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A6=AC=EB=B9=84?= Date: Fri, 16 Aug 2024 14:53:14 +0900 Subject: [PATCH 097/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20TODO=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: TODO 도메인 작성 및 검증 구현 * feat: 여행 계획 장소 TODO Repository 구현 * feat: TravelPlan 조회 응답에 PlaceTodo가 추가 * feat: 여행 계획 작성 시 TODO 요청 받도록 명세 수정 * style: TravelPlan DTO 클래스 이름 통일성 부여 개선 * feat: 멤버 도메인 hasId 기능 추가 * feat: TODO 체크 상태 업데이트 기능 추가 * feat: Todo의 author를 조회하는 레포지토리 기능 추가 * chore: 사용하지 않는 클래스 삭제 * feat: TODO update API 작성 * test: 멤버 fixture 수정 * test: 실패 테스트 수정 * feat: 여행 계획 생성 시 TODO가 체크 되지 않은 상태로 생성되도록 기능 추가 * test: TODO 체크 API 테스트 작성 * fix: 여행계획 수정 시 TODO check여부를 false로 생성하도록 수정 * test: 실패하는 Test 수정 (예외 명세 수정) * feat: 여행 계획 장소에서 description 속성 삭제 및 여행 계획 삭제 시 todo가 삭제되도록 기능 구현 * fix: 계획 생성 요청 DTO에서 TODO DTO가 null일 수 없도록 수정 * feat: TODO의 Owner를 찾는 QueryDsl을 기능 구현 * test: Todo List NotNull 제약 조건 추가에 따른 테스트 수정 및 QueryDsl을 사용한 서비스 테스트 수정 * refactor: 사용하지 않는 메서드 제거 개선 * style: member.hasId() -> member.isId() * fix: Todo List가 비어 있는지 null이 아닌 isEmpty로 확인하도록 수정 * refactor: 사용할 필요 없는 메서드 제거 개선 * fix: MemberAuth에서 직접 멤버를 조회하도록 수정 --- .../authentication/service/LoginService.java | 2 +- .../member/controller/MyPageController.java | 6 +- .../java/kr/touroot/member/domain/Member.java | 4 +- .../member/service/MyPageFacadeService.java | 4 +- .../controller/PlaceTodoController.java | 57 ++++++++++ .../controller/TravelPlanController.java | 20 ++-- .../travelplan/domain/TravelPlaceTodo.java | 96 ++++++++++++++++ .../travelplan/domain/TravelPlanPlace.java | 23 +--- ...ateRequest.java => PlanCreateRequest.java} | 2 +- .../dto/request/PlanPlaceCreateRequest.java | 10 +- .../dto/request/PlanPlaceTodoRequest.java | 19 ++++ .../dto/request/TodoStatusUpdateRequest.java | 11 ++ ...eResponse.java => PlanCreateResponse.java} | 2 +- ...nDayResponse.java => PlanDayResponse.java} | 10 +- ...ceResponse.java => PlanPlaceResponse.java} | 15 +-- .../dto/response/PlanPlaceTodoResponse.java | 23 ++++ ...esponse.java => PlanPositionResponse.java} | 6 +- ...velPlanResponse.java => PlanResponse.java} | 8 +- .../repository/PlaceTodoQueryRepository.java | 11 ++ .../PlaceTodoQueryRepositoryImpl.java | 35 ++++++ .../repository/PlaceTodoRepository.java | 14 +++ .../travelplan/service/PlaceTodoService.java | 45 ++++++++ .../travelplan/service/TravelPlanService.java | 60 +++++++--- .../authentication/fixture/MemberFixture.java | 22 ---- .../helper/LoginTestHelper.java | 4 +- .../service/LoginServiceTest.java | 23 ++-- .../global/config/TestQueryDslConfig.java | 7 ++ .../touroot/member/fixture/MemberFixture.java | 30 +++++ .../member/helper/MemberTestHelper.java | 7 +- .../member/service/MemberServiceTest.java | 8 +- .../travelogue/domain/TravelogueTest.java | 4 +- .../travelogue/fixture/TravelogueFixture.java | 4 +- .../helper/TravelogueTestHelper.java | 4 +- .../controller/PlaceTodoControllerTest.java | 106 ++++++++++++++++++ .../controller/TravelPlanControllerTest.java | 12 +- .../domain/TravelPlaceTodoTest.java | 98 ++++++++++++++++ .../domain/TravelPlanPlaceTest.java | 28 +---- .../travelplan/domain/TravelPlanTest.java | 4 +- .../travelplan/fixture/TravelPlanFixture.java | 4 +- .../fixture/TravelPlanPlaceFixture.java | 27 +++++ .../helper/TravelPlanTestHelper.java | 25 ++++- .../service/PlaceTodoServiceTest.java | 85 ++++++++++++++ .../service/TravelPlanServiceTest.java | 25 +++-- 43 files changed, 833 insertions(+), 177 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/travelplan/controller/PlaceTodoController.java create mode 100644 backend/src/main/java/kr/touroot/travelplan/domain/TravelPlaceTodo.java rename backend/src/main/java/kr/touroot/travelplan/dto/request/{TravelPlanCreateRequest.java => PlanCreateRequest.java} (97%) create mode 100644 backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java create mode 100644 backend/src/main/java/kr/touroot/travelplan/dto/request/TodoStatusUpdateRequest.java rename backend/src/main/java/kr/touroot/travelplan/dto/response/{TravelPlanCreateResponse.java => PlanCreateResponse.java} (82%) rename backend/src/main/java/kr/touroot/travelplan/dto/response/{TravelPlanDayResponse.java => PlanDayResponse.java} (67%) rename backend/src/main/java/kr/touroot/travelplan/dto/response/{TravelPlanPlaceResponse.java => PlanPlaceResponse.java} (54%) create mode 100644 backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceTodoResponse.java rename backend/src/main/java/kr/touroot/travelplan/dto/response/{TravelPlanPositionResponse.java => PlanPositionResponse.java} (78%) rename backend/src/main/java/kr/touroot/travelplan/dto/response/{TravelPlanResponse.java => PlanResponse.java} (81%) create mode 100644 backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepositoryImpl.java create mode 100644 backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelplan/service/PlaceTodoService.java delete mode 100644 backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java create mode 100644 backend/src/test/java/kr/touroot/member/fixture/MemberFixture.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/controller/PlaceTodoControllerTest.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/domain/TravelPlaceTodoTest.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java create mode 100644 backend/src/test/java/kr/touroot/travelplan/service/PlaceTodoServiceTest.java diff --git a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java index acbd7d6c..2b0c18fd 100644 --- a/backend/src/main/java/kr/touroot/authentication/service/LoginService.java +++ b/backend/src/main/java/kr/touroot/authentication/service/LoginService.java @@ -1,8 +1,8 @@ package kr.touroot.authentication.service; -import kr.touroot.authentication.dto.request.LoginRequest; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import kr.touroot.authentication.dto.request.LoginRequest; import kr.touroot.authentication.dto.request.TokenReissueRequest; import kr.touroot.authentication.dto.response.LoginResponse; import kr.touroot.authentication.dto.response.OauthUserInformationResponse; diff --git a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java index 219c0439..dce8b53e 100644 --- a/backend/src/main/java/kr/touroot/member/controller/MyPageController.java +++ b/backend/src/main/java/kr/touroot/member/controller/MyPageController.java @@ -15,7 +15,7 @@ import kr.touroot.member.dto.ProfileResponse; import kr.touroot.member.dto.request.ProfileUpdateRequest; import kr.touroot.member.service.MyPageFacadeService; -import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.dto.response.PlanResponse; import lombok.RequiredArgsConstructor; import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Page; @@ -93,13 +93,13 @@ public ResponseEntity> readTravelogues( }) @PageableAsQueryParam @GetMapping("/travel-plans") - public ResponseEntity> readTravelPlans( + public ResponseEntity> readTravelPlans( @NotNull MemberAuth memberAuth, @Parameter(hidden = true) @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable ) { - Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); + Page data = myPageFacadeService.readTravelPlans(memberAuth, pageable); return ResponseEntity.ok(data); } diff --git a/backend/src/main/java/kr/touroot/member/domain/Member.java b/backend/src/main/java/kr/touroot/member/domain/Member.java index 5859b974..6262d57d 100644 --- a/backend/src/main/java/kr/touroot/member/domain/Member.java +++ b/backend/src/main/java/kr/touroot/member/domain/Member.java @@ -11,12 +11,14 @@ import kr.touroot.global.entity.BaseEntity; import kr.touroot.global.exception.BadRequestException; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Member extends BaseEntity { diff --git a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java index ec9153b9..11081475 100644 --- a/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java +++ b/backend/src/main/java/kr/touroot/member/service/MyPageFacadeService.java @@ -8,7 +8,7 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.service.TravelogueService; import kr.touroot.travelplan.domain.TravelPlan; -import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.dto.response.PlanResponse; import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -39,7 +39,7 @@ public Page readTravelogues(MemberAuth memberAuth, Pageabl } @Transactional(readOnly = true) - public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { + public Page readTravelPlans(MemberAuth memberAuth, Pageable pageable) { Member member = memberService.getById(memberAuth.memberId()); Page travelPlans = travelPlanService.getAllByAuthor(member, pageable); diff --git a/backend/src/main/java/kr/touroot/travelplan/controller/PlaceTodoController.java b/backend/src/main/java/kr/touroot/travelplan/controller/PlaceTodoController.java new file mode 100644 index 00000000..be894188 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/controller/PlaceTodoController.java @@ -0,0 +1,57 @@ +package kr.touroot.travelplan.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.exception.dto.ExceptionResponse; +import kr.touroot.travelplan.dto.request.TodoStatusUpdateRequest; +import kr.touroot.travelplan.dto.response.PlanPlaceTodoResponse; +import kr.touroot.travelplan.service.PlaceTodoService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "여행 계획 장소의 TODO") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/todos") +public class PlaceTodoController { + + private final PlaceTodoService placeTodoService; + + @Operation(summary = "TODO 업데이트") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 TODO ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "작성자가 아닌 사용자가 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PatchMapping("/{id}") + public ResponseEntity updateTodo( + @PathVariable Long id, + @Valid @RequestBody TodoStatusUpdateRequest updateRequest, + MemberAuth memberAuth + ) { + PlanPlaceTodoResponse updatedTodoResponse = placeTodoService.updateTodoStatus(id, memberAuth, updateRequest); + return ResponseEntity.ok(updatedTodoResponse); + } +} diff --git a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java index 29fcd8ec..d8ee6202 100644 --- a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java @@ -12,9 +12,9 @@ import java.util.UUID; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; -import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; -import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.dto.request.PlanCreateRequest; +import kr.touroot.travelplan.dto.response.PlanCreateResponse; +import kr.touroot.travelplan.dto.response.PlanResponse; import kr.touroot.travelplan.service.TravelPlanService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -52,11 +52,11 @@ public class TravelPlanController { ) }) @PostMapping - public ResponseEntity createTravelPlan( - @Valid @RequestBody TravelPlanCreateRequest request, + public ResponseEntity createTravelPlan( + @Valid @RequestBody PlanCreateRequest request, MemberAuth memberAuth ) { - TravelPlanCreateResponse data = travelPlanService.createTravelPlan(request, memberAuth); + PlanCreateResponse data = travelPlanService.createTravelPlan(request, memberAuth); return ResponseEntity.created(URI.create("/api/v1/travel-plans/" + data.id())) .body(data); } @@ -79,11 +79,11 @@ public ResponseEntity createTravelPlan( ) }) @GetMapping("/{id}") - public ResponseEntity readTravelPlan( + public ResponseEntity readTravelPlan( @Parameter(description = "여행 계획 id") @PathVariable Long id, MemberAuth memberAuth ) { - TravelPlanResponse data = travelPlanService.readTravelPlan(id, memberAuth); + PlanResponse data = travelPlanService.readTravelPlan(id, memberAuth); return ResponseEntity.ok(data); } @@ -124,10 +124,10 @@ public ResponseEntity deleteTravelPlan(@PathVariable Long id, MemberAuth m ), }) @GetMapping("shared/{shareKey}") - public ResponseEntity readSharedTravelPlan( + public ResponseEntity readSharedTravelPlan( @Parameter(description = "여행 계획 공유 키") @PathVariable UUID shareKey ) { - TravelPlanResponse data = travelPlanService.readTravelPlan(shareKey); + PlanResponse data = travelPlanService.readTravelPlan(shareKey); return ResponseEntity.ok(data); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlaceTodo.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlaceTodo.java new file mode 100644 index 00000000..703e39be --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlaceTodo.java @@ -0,0 +1,96 @@ +package kr.touroot.travelplan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import kr.touroot.global.entity.BaseEntity; +import kr.touroot.global.exception.BadRequestException; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLDelete(sql = "UPDATE travel_place_todo SET deleted_at = NOW() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") +@Entity +public class TravelPlaceTodo extends BaseEntity { + + private static final int CONTENT_MIN_LENGTH = 1; + private static final int CONTENT_MAX_LENGTH = 20; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private TravelPlanPlace travelPlanPlace; + + @Column(nullable = false) + private String content; + + @Column(name = "TODO_ORDER", nullable = false) + private Integer order; + + @Column(nullable = false) + private Boolean isChecked; + + private TravelPlaceTodo(Long id, TravelPlanPlace travelPlanPlace, String content, Integer order, + Boolean isChecked) { + validate(travelPlanPlace, content, order, isChecked); + this.id = id; + this.travelPlanPlace = travelPlanPlace; + this.content = content; + this.order = order; + this.isChecked = isChecked; + } + + public TravelPlaceTodo(TravelPlanPlace travelPlanPlace, String content, Integer order, Boolean isChecked) { + this(null, travelPlanPlace, content, order, isChecked); + } + + private void validate(TravelPlanPlace travelPlanPlace, String content, Integer order, Boolean isChecked) { + validateNotNull(travelPlanPlace, content, order, isChecked); + validateNotBlank(content); + validateContentLength(content); + validateOrderNonNegative(order); + } + + private void validateNotNull(TravelPlanPlace travelPlanPlace, String content, Integer order, Boolean isChecked) { + if (travelPlanPlace == null || content == null || order == null || isChecked == null) { + throw new BadRequestException("여행 계획 장소에 대한 TODO에서 장소와 내용, 순서 그리고 달성 여부는 비어 있을 수 없습니다"); + } + } + + private void validateNotBlank(String content) { + if (content.isBlank()) { + throw new BadRequestException("TODO 내용은 빈 문자열로만 이루어질 수 없습니다"); + } + } + + private void validateContentLength(String content) { + if (CONTENT_MIN_LENGTH > content.length() || content.length() > CONTENT_MAX_LENGTH) { + throw new BadRequestException( + "TODO 내용의 길이는 " + CONTENT_MIN_LENGTH + "자 이상, " + CONTENT_MAX_LENGTH + "자 이하여야 합니다" + ); + } + } + + private void validateOrderNonNegative(Integer order) { + if (order < 0) { + throw new BadRequestException("TODO 순서는 음수일 수 없습니다"); + } + } + + public void updateCheckedStatus(boolean checkedStatus) { + isChecked = checkedStatus; + } +} diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java index 15808cd1..1512036c 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlanPlace.java @@ -24,15 +24,10 @@ @Entity public class TravelPlanPlace extends BaseEntity { - private static final int MAX_DESCRIPTION_LENGTH = 300; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(columnDefinition = "VARCHAR(300)") - private String description; - @Column(name = "PLAN_PLACE_ORDER", nullable = false) private Integer order; @@ -44,22 +39,20 @@ public class TravelPlanPlace extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private Place place; - private TravelPlanPlace(Long id, String description, Integer order, TravelPlanDay day, Place place) { - validate(description, order, day, place); + private TravelPlanPlace(Long id, Integer order, TravelPlanDay day, Place place) { + validate(order, day, place); this.id = id; - this.description = description; this.order = order; this.day = day; this.place = place; } - public TravelPlanPlace(String description, Integer order, TravelPlanDay day, Place place) { - this(null, description, order, day, place); + public TravelPlanPlace(Integer order, TravelPlanDay day, Place place) { + this(null, order, day, place); } - private void validate(String description, Integer order, TravelPlanDay day, Place place) { + private void validate(Integer order, TravelPlanDay day, Place place) { validateNotNull(order, day, place); - validateDescriptionLength(description); validateOrderRange(order); } @@ -69,12 +62,6 @@ private void validateNotNull(Integer order, TravelPlanDay day, Place place) { } } - private void validateDescriptionLength(String description) { - if (description != null && description.length() > MAX_DESCRIPTION_LENGTH) { - throw new BadRequestException("장소 설명은 " + MAX_DESCRIPTION_LENGTH + "자를 넘을 수 없습니다"); - } - } - private void validateOrderRange(Integer order) { if (order < 0) { throw new BadRequestException("장소의 방문 순서는 음수일 수 없습니다"); diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanCreateRequest.java similarity index 97% rename from backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanCreateRequest.java index ce493c83..fff10f21 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/TravelPlanCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanCreateRequest.java @@ -13,7 +13,7 @@ import lombok.Builder; @Builder -public record TravelPlanCreateRequest( +public record PlanCreateRequest( @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") @NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.") String title, diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java index 718e2e67..d6ea332d 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import java.util.List; import kr.touroot.place.domain.Place; import kr.touroot.travelplan.domain.TravelPlanDay; import kr.touroot.travelplan.domain.TravelPlanPlace; @@ -13,15 +14,16 @@ public record PlanPlaceCreateRequest( @Schema(description = "여행 장소 이름", example = "잠실한강공원") @NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName, - @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") - String description, @Valid @NotNull(message = "위치는 비어있을 수 없습니다.") - PlanPositionCreateRequest position + PlanPositionCreateRequest position, + @Valid + @NotNull(message = "TODO 리스트는 필수 입니다.") + List todos ) { public TravelPlanPlace toPlanPlace(int order, TravelPlanDay day, Place place) { - return new TravelPlanPlace(description, order, day, place); + return new TravelPlanPlace(order, day, place); } public Place toPlace() { diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java new file mode 100644 index 00000000..a9b9131e --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java @@ -0,0 +1,19 @@ +package kr.touroot.travelplan.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import kr.touroot.travelplan.domain.TravelPlaceTodo; +import kr.touroot.travelplan.domain.TravelPlanPlace; + +public record PlanPlaceTodoRequest( + @Schema(description = "여행 장소에서 진행할 TODO", example = "함덕 해수욕장 산책") + @NotBlank(message = "TODO 내용은 비어 있을 수 없습니다") + @Size(min = 1, max = 20, message = "TODO 내용은 1자에서 20자 사이의 길이를 가져야 합니다") + String content +) { + + public TravelPlaceTodo toUncheckedPlaceTodo(TravelPlanPlace travelPlanPlace, Integer order) { + return new TravelPlaceTodo(travelPlanPlace, content, order, false); + } +} diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/TodoStatusUpdateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/TodoStatusUpdateRequest.java new file mode 100644 index 00000000..c30eb4a7 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/TodoStatusUpdateRequest.java @@ -0,0 +1,11 @@ +package kr.touroot.travelplan.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record TodoStatusUpdateRequest( + @Schema(description = "업데이트 하고자 하는 체크 상태", example = "true") + @NotNull + Boolean checked +) { +} diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanCreateResponse.java similarity index 82% rename from backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/PlanCreateResponse.java index fbfaada1..f742bb72 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanCreateResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanCreateResponse.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record TravelPlanCreateResponse( +public record PlanCreateResponse( @Schema(description = "생성된 여행 계획 id", example = "1") Long id ) { diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanDayResponse.java similarity index 67% rename from backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/PlanDayResponse.java index c3044dcd..1b90a266 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanDayResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanDayResponse.java @@ -7,17 +7,17 @@ import lombok.Builder; @Builder -public record TravelPlanDayResponse( +public record PlanDayResponse( @Schema(description = "여행 날짜 Id") Long id, @Schema(description = "여행 일자") LocalDate date, - @Schema(description = "여행 장소별 정보") List places + @Schema(description = "여행 장소별 정보") List places ) { - public static TravelPlanDayResponse of( + public static PlanDayResponse of( TravelPlanDay planDay, - List places + List places ) { - return TravelPlanDayResponse.builder() + return PlanDayResponse.builder() .id(planDay.getId()) .date(planDay.getCurrentDate()) .places(places) diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java similarity index 54% rename from backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java index ac037f22..f36dfd8a 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPlaceResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceResponse.java @@ -1,27 +1,28 @@ package kr.touroot.travelplan.dto.response; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; import kr.touroot.place.domain.Place; import kr.touroot.travelplan.domain.TravelPlanPlace; import lombok.Builder; @Builder -public record TravelPlanPlaceResponse( +public record PlanPlaceResponse( @Schema(description = "여행 장소 Id", example = "1") Long id, @Schema(description = "여행 장소 이름", example = "잠실한강공원") String placeName, - @Schema(description = "여행 장소 위치") TravelPlanPositionResponse position, - @Schema(description = "여행 장소 설명", example = "신나는 여행 장소") String description + @Schema(description = "여행 장소 위치") PlanPositionResponse position, + @Schema(description = "여행 장소 TODO") List todos ) { - public static TravelPlanPlaceResponse from(TravelPlanPlace planPlace) { + public static PlanPlaceResponse of(TravelPlanPlace planPlace, List todos) { Place place = planPlace.getPlace(); - TravelPlanPositionResponse locationResponse = TravelPlanPositionResponse.from(place); + PlanPositionResponse locationResponse = PlanPositionResponse.from(place); - return TravelPlanPlaceResponse.builder() + return PlanPlaceResponse.builder() .id(planPlace.getId()) .placeName(place.getName()) .position(locationResponse) - .description(planPlace.getDescription()) + .todos(todos) .build(); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceTodoResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceTodoResponse.java new file mode 100644 index 00000000..95040ff2 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPlaceTodoResponse.java @@ -0,0 +1,23 @@ +package kr.touroot.travelplan.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import kr.touroot.travelplan.domain.TravelPlaceTodo; +import lombok.Builder; + +@Builder +public record PlanPlaceTodoResponse( + @Schema(description = "TODO 아이디") Long id, + @Schema(description = "TODO 내용") String content, + @Schema(description = "TODO 순서") Integer order, + @Schema(description = "TODO 체크 여부") Boolean checked +) { + + public static PlanPlaceTodoResponse from(TravelPlaceTodo travelPlaceTodo) { + return PlanPlaceTodoResponse.builder(). + id(travelPlaceTodo.getId()) + .content(travelPlaceTodo.getContent()) + .order(travelPlaceTodo.getOrder()) + .checked(travelPlaceTodo.getIsChecked()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPositionResponse.java similarity index 78% rename from backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPositionResponse.java index 39721f30..e765d569 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanPositionResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanPositionResponse.java @@ -5,14 +5,14 @@ import lombok.Builder; @Builder -public record TravelPlanPositionResponse( +public record PlanPositionResponse( @Schema(description = "여행 획 장소 상세 Id", example = "1") Long id, @Schema(description = "여행 장소 위도", example = "37.5175896") String lat, @Schema(description = "여행 계획 경도", example = "127.0867236") String lng ) { - public static TravelPlanPositionResponse from(Place place) { - return TravelPlanPositionResponse.builder() + public static PlanPositionResponse from(Place place) { + return PlanPositionResponse.builder() .id(place.getId()) .lat(place.getLatitude()) .lng(place.getLongitude()) diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanResponse.java similarity index 81% rename from backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java rename to backend/src/main/java/kr/touroot/travelplan/dto/response/PlanResponse.java index c9cb3e2b..b3f9937a 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/response/TravelPlanResponse.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/response/PlanResponse.java @@ -8,16 +8,16 @@ import lombok.Builder; @Builder -public record TravelPlanResponse( +public record PlanResponse( @Schema(description = "여행 계획 id", example = "1") Long id, @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") String title, @Schema(description = "여행 시작일", example = "2024-11-16") LocalDate startDate, - @Schema(description = "여행 계획 날짜별 정보") List days, + @Schema(description = "여행 계획 날짜별 정보") List days, @Schema(description = "여행 계획 공유 share Key") UUID shareKey ) { - public static TravelPlanResponse of(TravelPlan travelPlan, List days) { - return TravelPlanResponse.builder() + public static PlanResponse of(TravelPlan travelPlan, List days) { + return PlanResponse.builder() .id(travelPlan.getId()) .title(travelPlan.getTitle()) .startDate(travelPlan.getStartDate()) diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepository.java new file mode 100644 index 00000000..6b689c5f --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepository.java @@ -0,0 +1,11 @@ +package kr.touroot.travelplan.repository; + +import java.util.Optional; +import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlaceTodo; +import org.springframework.data.repository.query.Param; + +public interface PlaceTodoQueryRepository { + + Optional findOwnerOf(@Param("placeTodo") TravelPlaceTodo placeTodo); +} diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepositoryImpl.java new file mode 100644 index 00000000..c6df82f9 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoQueryRepositoryImpl.java @@ -0,0 +1,35 @@ +package kr.touroot.travelplan.repository; + +import static kr.touroot.member.domain.QMember.member; +import static kr.touroot.travelplan.domain.QTravelPlaceTodo.travelPlaceTodo; +import static kr.touroot.travelplan.domain.QTravelPlan.travelPlan; +import static kr.touroot.travelplan.domain.QTravelPlanDay.travelPlanDay; +import static kr.touroot.travelplan.domain.QTravelPlanPlace.travelPlanPlace; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlaceTodo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class PlaceTodoQueryRepositoryImpl implements PlaceTodoQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Optional findOwnerOf(TravelPlaceTodo placeTodo) { + return Optional.ofNullable( + jpaQueryFactory.select(member) + .from(member) + .innerJoin(travelPlan).on(travelPlan.author.eq(member)) + .innerJoin(travelPlanDay).on(travelPlanDay.plan.eq(travelPlan)) + .innerJoin(travelPlanPlace).on(travelPlanPlace.day.eq(travelPlanDay)) + .innerJoin(travelPlaceTodo).on(travelPlaceTodo.travelPlanPlace.eq(travelPlanPlace)) + .where(travelPlaceTodo.eq(placeTodo)) + .fetchOne() + ); + } +} diff --git a/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoRepository.java b/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoRepository.java new file mode 100644 index 00000000..21467827 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/repository/PlaceTodoRepository.java @@ -0,0 +1,14 @@ +package kr.touroot.travelplan.repository; + +import java.util.List; +import kr.touroot.travelplan.domain.TravelPlaceTodo; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.domain.TravelPlanPlace; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlaceTodoRepository extends JpaRepository { + + List findByTravelPlanPlace(TravelPlanPlace travelPlanPlace); + + void deleteByTravelPlanPlaceDayPlan(TravelPlan travelPlan); +} diff --git a/backend/src/main/java/kr/touroot/travelplan/service/PlaceTodoService.java b/backend/src/main/java/kr/touroot/travelplan/service/PlaceTodoService.java new file mode 100644 index 00000000..8bdbd406 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelplan/service/PlaceTodoService.java @@ -0,0 +1,45 @@ +package kr.touroot.travelplan.service; + +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; +import kr.touroot.member.domain.Member; +import kr.touroot.member.repository.MemberRepository; +import kr.touroot.travelplan.domain.TravelPlaceTodo; +import kr.touroot.travelplan.dto.request.TodoStatusUpdateRequest; +import kr.touroot.travelplan.dto.response.PlanPlaceTodoResponse; +import kr.touroot.travelplan.repository.PlaceTodoQueryRepository; +import kr.touroot.travelplan.repository.PlaceTodoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class PlaceTodoService { + + private final PlaceTodoRepository placeTodoRepository; + private final PlaceTodoQueryRepository placeTodoQueryRepository; + private final MemberRepository memberRepository; + + @Transactional + public PlanPlaceTodoResponse updateTodoStatus( + Long id, + MemberAuth memberAuth, + TodoStatusUpdateRequest updateRequest + ) { + Member accessor = memberRepository.findById(memberAuth.memberId()) + .orElseThrow(() -> new BadRequestException("존재하지 않는 사용자입니다.")); + TravelPlaceTodo todo = placeTodoRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 TODO 입니다")); + Member owner = placeTodoQueryRepository.findOwnerOf(todo) + .orElseThrow(() -> new BadRequestException("TODO 작성자가 존재하지 않습니다")); + + if (!owner.equals(accessor)) { + throw new ForbiddenException("TODO 체크는 작성자만 가능합니다"); + } + + todo.updateCheckedStatus(updateRequest.checked()); + return PlanPlaceTodoResponse.from(todo); + } +} diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index c75c6605..f1643866 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -11,16 +11,20 @@ import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelplan.domain.TravelPlaceTodo; import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.domain.TravelPlanDay; import kr.touroot.travelplan.domain.TravelPlanPlace; +import kr.touroot.travelplan.dto.request.PlanCreateRequest; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; -import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; -import kr.touroot.travelplan.dto.response.TravelPlanDayResponse; -import kr.touroot.travelplan.dto.response.TravelPlanPlaceResponse; -import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.dto.request.PlanPlaceTodoRequest; +import kr.touroot.travelplan.dto.response.PlanCreateResponse; +import kr.touroot.travelplan.dto.response.PlanDayResponse; +import kr.touroot.travelplan.dto.response.PlanPlaceResponse; +import kr.touroot.travelplan.dto.response.PlanPlaceTodoResponse; +import kr.touroot.travelplan.dto.response.PlanResponse; +import kr.touroot.travelplan.repository.PlaceTodoRepository; import kr.touroot.travelplan.repository.TravelPlanDayRepository; import kr.touroot.travelplan.repository.TravelPlanPlaceRepository; import kr.touroot.travelplan.repository.TravelPlanRepository; @@ -39,9 +43,10 @@ public class TravelPlanService { private final TravelPlanDayRepository travelPlanDayRepository; private final TravelPlanPlaceRepository travelPlanPlaceRepository; private final PlaceRepository placeRepository; + private final PlaceTodoRepository placeTodoRepository; @Transactional - public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request, MemberAuth memberAuth) { + public PlanCreateResponse createTravelPlan(PlanCreateRequest request, MemberAuth memberAuth) { Member author = getMemberByMemberAuth(memberAuth); TravelPlan travelPlan = request.toTravelPlan(author, UUID.randomUUID()); validateTravelPlan(travelPlan); @@ -49,7 +54,7 @@ public TravelPlanCreateResponse createTravelPlan(TravelPlanCreateRequest request TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); createPlanDay(request.days(), savedTravelPlan); - return new TravelPlanCreateResponse(savedTravelPlan.getId()); + return new PlanCreateResponse(savedTravelPlan.getId()); } private void validateTravelPlan(TravelPlan travelPlan) { @@ -75,7 +80,17 @@ private void createPlanPlace(List request, TravelPlanDay for (int order = 0; order < request.size(); order++) { PlanPlaceCreateRequest planRequest = request.get(order); Place place = getPlace(planRequest); - travelPlanPlaceRepository.save(planRequest.toPlanPlace(order, travelPlanDay, place)); + TravelPlanPlace planPlace = planRequest.toPlanPlace(order, travelPlanDay, place); + TravelPlanPlace travelPlanPlace = travelPlanPlaceRepository.save(planPlace); + createPlaceTodo(planRequest.todos(), travelPlanPlace); + } + } + + private void createPlaceTodo(List request, TravelPlanPlace travelPlanPlace) { + for (int order = 0; order < request.size(); order++) { + PlanPlaceTodoRequest todoRequest = request.get(order); + TravelPlaceTodo travelPlaceTodo = todoRequest.toUncheckedPlaceTodo(travelPlanPlace, order); + placeTodoRepository.save(travelPlaceTodo); } } @@ -88,12 +103,12 @@ private Place getPlace(PlanPlaceCreateRequest planRequest) { } @Transactional(readOnly = true) - public TravelPlanResponse readTravelPlan(Long planId, MemberAuth memberAuth) { + public PlanResponse readTravelPlan(Long planId, MemberAuth memberAuth) { TravelPlan travelPlan = getTravelPlanById(planId); Member member = getMemberByMemberAuth(memberAuth); validateReadByAuthor(travelPlan, member); - return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + return PlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } private void validateReadByAuthor(TravelPlan travelPlan, Member member) { @@ -103,10 +118,10 @@ private void validateReadByAuthor(TravelPlan travelPlan, Member member) { } @Transactional(readOnly = true) - public TravelPlanResponse readTravelPlan(UUID shareKey) { + public PlanResponse readTravelPlan(UUID shareKey) { TravelPlan travelPlan = getTravelPlanByShareKey(shareKey); - return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + return PlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } private TravelPlan getTravelPlanById(Long planId) { @@ -119,25 +134,33 @@ private TravelPlan getTravelPlanByShareKey(UUID shareKey) { .orElseThrow(() -> new BadRequestException("존재하지 않는 여행 계획입니다.")); } - public TravelPlanResponse getTravelPlanResponse(TravelPlan travelPlan) { - return TravelPlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); + public PlanResponse getTravelPlanResponse(TravelPlan travelPlan) { + return PlanResponse.of(travelPlan, getTravelPlanDayResponses(travelPlan)); } - private List getTravelPlanDayResponses(TravelPlan travelPlan) { + private List getTravelPlanDayResponses(TravelPlan travelPlan) { List planDays = travelPlanDayRepository.findByPlan(travelPlan); return planDays.stream() .sorted(Comparator.comparing(TravelPlanDay::getOrder)) - .map(day -> TravelPlanDayResponse.of(day, getTravelPlanPlaceResponses(day))) + .map(day -> PlanDayResponse.of(day, getTravelPlanPlaceResponses(day))) .toList(); } - private List getTravelPlanPlaceResponses(TravelPlanDay day) { + private List getTravelPlanPlaceResponses(TravelPlanDay day) { List places = travelPlanPlaceRepository.findByDay(day); return places.stream() .sorted(Comparator.comparing(TravelPlanPlace::getOrder)) - .map(TravelPlanPlaceResponse::from) + .map(place -> PlanPlaceResponse.of(place, getPlaceTodos(place))) + .toList(); + } + + private List getPlaceTodos(TravelPlanPlace place) { + return placeTodoRepository.findByTravelPlanPlace(place) + .stream() + .sorted(Comparator.comparing(TravelPlaceTodo::getOrder)) + .map(PlanPlaceTodoResponse::from) .toList(); } @@ -156,6 +179,7 @@ public void deleteByTravelPlanId(Long planId, MemberAuth memberAuth) { Member author = getMemberByMemberAuth(memberAuth); validateDeleteByAuthor(travelPlan, author); + placeTodoRepository.deleteByTravelPlanPlaceDayPlan(travelPlan); travelPlanPlaceRepository.deleteByDayPlan(travelPlan); travelPlanDayRepository.deleteByPlan(travelPlan); travelPlanRepository.delete(travelPlan); diff --git a/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java b/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java deleted file mode 100644 index 2e240158..00000000 --- a/backend/src/test/java/kr/touroot/authentication/fixture/MemberFixture.java +++ /dev/null @@ -1,22 +0,0 @@ -package kr.touroot.authentication.fixture; - -import kr.touroot.member.domain.LoginType; -import kr.touroot.member.domain.Member; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public enum MemberFixture { - - MEMBER_KAKAO(new Member(1L, "리비", "https://dev.touroot.kr/temporary/profile.png", LoginType.KAKAO)), - MEMBER_DEFAULT(new Member( - "user@email.com", - "5304d46adc", - "뚜리", - "https://dev.touroot.kr/temporary/profile.png", - LoginType.DEFAULT - )); - - private final Member member; -} diff --git a/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java b/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java index 46e1da67..a29fae6d 100644 --- a/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java +++ b/backend/src/test/java/kr/touroot/authentication/helper/LoginTestHelper.java @@ -1,7 +1,7 @@ package kr.touroot.authentication.helper; -import kr.touroot.authentication.fixture.MemberFixture; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import kr.touroot.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,7 +13,7 @@ public class LoginTestHelper { private final MemberRepository memberRepository; public Member initMemberTestData() { - Member member = MemberFixture.MEMBER_DEFAULT.getMember(); + Member member = MemberFixture.DEFAULT_MEMBER.build(); return memberRepository.save(member); } } diff --git a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java index 8595d973..b133c3e2 100644 --- a/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java +++ b/backend/src/test/java/kr/touroot/authentication/service/LoginServiceTest.java @@ -1,6 +1,5 @@ package kr.touroot.authentication.service; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -8,12 +7,13 @@ import static org.mockito.Mockito.when; import java.util.Optional; -import kr.touroot.authentication.dto.response.TokenResponse; import kr.touroot.authentication.dto.response.LoginResponse; +import kr.touroot.authentication.dto.response.TokenResponse; import kr.touroot.authentication.fixture.OauthUserInformationFixture; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.authentication.infrastructure.KakaoOauthProvider; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import kr.touroot.member.repository.MemberRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,6 +28,7 @@ class LoginServiceTest { private static final String AUTHENTICATION_CODE = "test-authentication-code"; private static final String REDIRECT_URI = "http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fv1%2Flogin%2Foauth%2Fkakao"; + private static final Member MEMBER = MemberFixture.KAKAO_MEMBER.build(); @InjectMocks private LoginService loginService; @@ -48,18 +49,15 @@ void existUserKakaoSocialLoginTest() { when(kakaoOauthProvider.getUserInformation(any(String.class), any(String.class))) .thenReturn(OauthUserInformationFixture.USER_1_OAUTH_INFORMATION); when(memberRepository.findByKakaoId(any(Long.class))) - .thenReturn(Optional.of(MEMBER_KAKAO.getMember())); - when(jwtTokenProvider.createToken(MEMBER_KAKAO.getMember().getId())) + .thenReturn(Optional.of(MEMBER)); + when(jwtTokenProvider.createToken(MEMBER.getId())) .thenReturn(new TokenResponse(accessToken, refreshToken)); LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( - LoginResponse.of( - MEMBER_KAKAO.getMember(), - new TokenResponse(response.accessToken(), response.refreshToken()) - ) + LoginResponse.of(MEMBER, new TokenResponse(response.accessToken(), response.refreshToken())) ); } @@ -75,18 +73,15 @@ void nonExistUserKakaoSocialLoginTest() { when(memberRepository.findByKakaoId(any(Long.class))) .thenReturn(Optional.empty()); when(memberRepository.save(any(Member.class))) - .thenReturn(MEMBER_KAKAO.getMember()); - when(jwtTokenProvider.createToken(MEMBER_KAKAO.getMember().getId())) + .thenReturn(MEMBER); + when(jwtTokenProvider.createToken(MEMBER.getId())) .thenReturn(new TokenResponse(accessToken, refreshToken)); LoginResponse response = loginService.login(AUTHENTICATION_CODE, REDIRECT_URI); // when & then assertThat(response).isEqualTo( - LoginResponse.of( - MEMBER_KAKAO.getMember(), - new TokenResponse(response.accessToken(), response.refreshToken()) - ) + LoginResponse.of(MEMBER, new TokenResponse(response.accessToken(), response.refreshToken())) ); verify(memberRepository, times(1)).save(any(Member.class)); } diff --git a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java index 4fc14dac..4be2bd08 100644 --- a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java +++ b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java @@ -5,6 +5,8 @@ import jakarta.persistence.PersistenceContext; import kr.touroot.travelogue.repository.TravelogueQueryRepository; import kr.touroot.travelogue.repository.TravelogueQueryRepositoryImpl; +import kr.touroot.travelplan.repository.PlaceTodoQueryRepository; +import kr.touroot.travelplan.repository.PlaceTodoQueryRepositoryImpl; import lombok.RequiredArgsConstructor; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -25,4 +27,9 @@ public JPAQueryFactory jpaQueryFactory() { public TravelogueQueryRepository travelogueQueryRepository() { return new TravelogueQueryRepositoryImpl(jpaQueryFactory()); } + + @Bean + public PlaceTodoQueryRepository placeTodoQueryRepository() { + return new PlaceTodoQueryRepositoryImpl(jpaQueryFactory()); + } } diff --git a/backend/src/test/java/kr/touroot/member/fixture/MemberFixture.java b/backend/src/test/java/kr/touroot/member/fixture/MemberFixture.java new file mode 100644 index 00000000..0fb3c440 --- /dev/null +++ b/backend/src/test/java/kr/touroot/member/fixture/MemberFixture.java @@ -0,0 +1,30 @@ +package kr.touroot.member.fixture; + +import kr.touroot.member.domain.LoginType; +import kr.touroot.member.domain.Member; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum MemberFixture { + + KAKAO_MEMBER(1L, null, null, "https://dev.touroot.kr/temporary/profile.png", "리비", + LoginType.KAKAO), + DEFAULT_MEMBER(null, "email@gmail.com", "password", "https://dev.touroot.kr/temporary/profile.png", "뚜리", + LoginType.DEFAULT); + + private final Long socialId; + private final String email; + private final String password; + private final String profileImageUrl; + private final String nickname; + private final LoginType loginType; + + public Member build() { + if (loginType == LoginType.KAKAO) { + return new Member(socialId, nickname, profileImageUrl, loginType); + } + return new Member(email, password, nickname, profileImageUrl, loginType); + } +} diff --git a/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java b/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java index ebb9ac75..cc6908d6 100644 --- a/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java +++ b/backend/src/test/java/kr/touroot/member/helper/MemberTestHelper.java @@ -1,6 +1,6 @@ package kr.touroot.member.helper; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_DEFAULT; +import static kr.touroot.member.fixture.MemberFixture.DEFAULT_MEMBER; import kr.touroot.member.domain.Member; import kr.touroot.member.repository.MemberRepository; @@ -18,8 +18,11 @@ public MemberTestHelper(MemberRepository memberRepository) { } public Member persistMember() { - Member member = MEMBER_DEFAULT.getMember(); + Member member = DEFAULT_MEMBER.build(); + return memberRepository.save(member); + } + public Member persistMember(Member member) { return memberRepository.save(member); } } diff --git a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java index ffb8b24c..f97ad67c 100644 --- a/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java +++ b/backend/src/test/java/kr/touroot/member/service/MemberServiceTest.java @@ -1,6 +1,5 @@ package kr.touroot.member.service; -import static kr.touroot.member.fixture.MemberRequestFixture.DUPLICATE_EMAIL_MEMBER; import static kr.touroot.member.fixture.MemberRequestFixture.DUPLICATE_NICKNAME_MEMBER; import static kr.touroot.member.fixture.MemberRequestFixture.VALID_MEMBER; import static org.assertj.core.api.Assertions.assertThat; @@ -75,10 +74,11 @@ void createMember() { @DisplayName("중복된 이메일을 가진 회원을 생성하려하면 예외가 발생한다.") @Test void createMemberWithDuplicatedEmail() { - testHelper.persistMember(); - MemberRequest request = DUPLICATE_EMAIL_MEMBER.getRequest(); + Member member = testHelper.persistMember(); + MemberRequest duplicateRequest = new MemberRequest(member.getEmail(), "testPassword", "nickname", + "https://dev.touroot.kr/images/f8c26e9f.png"); - assertThatThrownBy(() -> memberService.createMember(request)) + assertThatThrownBy(() -> memberService.createMember(duplicateRequest)) .isInstanceOf(BadRequestException.class) .hasMessage("이미 회원 가입되어 있는 이메일입니다."); } diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java index 8212fd0d..1b036965 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java @@ -1,11 +1,11 @@ package kr.touroot.travelogue.domain; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -14,7 +14,7 @@ @DisplayName("여행기 도메인") class TravelogueTest { - private static final Member VALID_AUTHOR = MEMBER_KAKAO.getMember(); + private static final Member VALID_AUTHOR = MemberFixture.KAKAO_MEMBER.build(); private static final String VALID_TITLE = "올바른 여행기 제목"; private static final String VALID_THUMBNAIL = "http://valid-thumbnail.com"; diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java index b36c1918..bc356c8f 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueFixture.java @@ -1,15 +1,15 @@ package kr.touroot.travelogue.fixture; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import kr.touroot.travelogue.domain.Travelogue; import lombok.AllArgsConstructor; @AllArgsConstructor public enum TravelogueFixture { - TRAVELOGUE(MEMBER_KAKAO.getMember(), "제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png"); + TRAVELOGUE(MemberFixture.KAKAO_MEMBER.build(), "제주에 하영 옵서", "https://dev.touroot.kr/temporary/jeju_thumbnail.png"); private final Member author; private final String title; diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 47028b70..7888d14b 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -1,6 +1,5 @@ package kr.touroot.travelogue.helper; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static kr.touroot.place.fixture.PlaceFixture.PLACE; import static kr.touroot.travelogue.fixture.TravelogueDayFixture.TRAVELOGUE_DAY; import static kr.touroot.travelogue.fixture.TravelogueFixture.TRAVELOGUE; @@ -9,6 +8,7 @@ import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; import kr.touroot.place.repository.PlaceRepository; @@ -99,7 +99,7 @@ private void persisTravelogueTag(Travelogue travelogue) { } public Member persistMember() { - Member author = MEMBER_KAKAO.getMember(); + Member author = MemberFixture.KAKAO_MEMBER.build(); return memberRepository.save(author); } diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/PlaceTodoControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/PlaceTodoControllerTest.java new file mode 100644 index 00000000..9811645b --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/controller/PlaceTodoControllerTest.java @@ -0,0 +1,106 @@ +package kr.touroot.travelplan.controller; + + +import static org.hamcrest.Matchers.is; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import kr.touroot.authentication.infrastructure.JwtTokenProvider; +import kr.touroot.global.AcceptanceTest; +import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.dto.request.TodoStatusUpdateRequest; +import kr.touroot.travelplan.helper.TravelPlanTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; + +@DisplayName("여행 계획 장소에 대한 TODO 컨트롤러") +@AcceptanceTest +class PlaceTodoControllerTest { + + private final ObjectMapper objectMapper; + private final DatabaseCleaner databaseCleaner; + private final JwtTokenProvider jwtTokenProvider; + private final TravelPlanTestHelper testHelper; + @LocalServerPort + private int port; + private String accessToken; + private Member member; + + @Autowired + public PlaceTodoControllerTest(ObjectMapper objectMapper, DatabaseCleaner databaseCleaner, + JwtTokenProvider jwtTokenProvider, TravelPlanTestHelper testHelper) { + this.objectMapper = objectMapper; + this.databaseCleaner = databaseCleaner; + this.jwtTokenProvider = jwtTokenProvider; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + RestAssured.port = port; + databaseCleaner.executeTruncate(); + + member = testHelper.initMemberTestData(); + accessToken = jwtTokenProvider.createToken(member.getId()).accessToken(); + } + + @DisplayName("TODO의 체크 상태를 수정할 수 있다") + @Test + void updateTodoStatus() { + testHelper.initTravelPlanTestData(member); + TodoStatusUpdateRequest todoStatusUpdateRequest = new TodoStatusUpdateRequest(true); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(todoStatusUpdateRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .patch("/api/v1/todos/" + 1L) + .then().log().all() + .statusCode(200) + .body("checked", is(true)); + } + + @DisplayName("존재하지 않는 TODO의 체크 상태를 업데이트 하려는 경우 예외가 발생한다") + @Test + void updateNonExistTodoStatus() { + TodoStatusUpdateRequest todoStatusUpdateRequest = new TodoStatusUpdateRequest(true); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(todoStatusUpdateRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().log().all() + .patch("/api/v1/todos/" + 1L) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 TODO 입니다")); + } + + @DisplayName("TODO 작성자가 아닌 멤버가 TODO의 체크 상태를 업데이트 하려는 경우 예외가 발생한다.") + @Test + void updateTodoStatusFromNonAuthor() { + testHelper.initTravelPlanTestData(member); + + TodoStatusUpdateRequest todoStatusUpdateRequest = new TodoStatusUpdateRequest(true); + Member notAuthor = testHelper.initMemberTestData(); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()) + .accessToken(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(todoStatusUpdateRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + notAuthorAccessToken) + .when().log().all() + .patch("/api/v1/todos/" + 1L) + .then().log().all() + .statusCode(403) + .body("message", is("TODO 체크는 작성자만 가능합니다")); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index 69a3c708..a89b55c5 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -6,16 +6,17 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.time.LocalDate; +import java.util.Collections; import java.util.List; import java.util.UUID; import kr.touroot.authentication.infrastructure.JwtTokenProvider; import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.dto.request.PlanCreateRequest; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; -import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; import kr.touroot.travelplan.helper.TravelPlanTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; @@ -67,11 +68,12 @@ void createTravelPlan() { PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") - .description("신나는 여행 장소") + .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); + PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + PlanCreateRequest request = PlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MAX) .days(List.of(planDayCreateRequest)) @@ -96,11 +98,11 @@ void createTravelPlanWithInvalidStartDate() { PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") - .description("신나는 여행 장소") + .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + PlanCreateRequest request = PlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MIN) .days(List.of(planDayCreateRequest)) diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlaceTodoTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlaceTodoTest.java new file mode 100644 index 00000000..4d3362ce --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlaceTodoTest.java @@ -0,0 +1,98 @@ +package kr.touroot.travelplan.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.travelplan.fixture.TravelPlanPlaceFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DisplayName("여행 계획 장소의 TODO") +class TravelPlaceTodoTest { + + private static final TravelPlanPlace VALID_PLACE = TravelPlanPlaceFixture.TRAVEL_PLAN_PLACE.get(); + private static final String VALID_CONTENT = "마라탕 먹기"; + private static final Integer VALID_ORDER = 0; + private static final Boolean VALID_CHECK = Boolean.TRUE; + private static final String EMPTY_ERROR_MESSAGE = "여행 계획 장소에 대한 TODO에서 장소와 내용, 순서 그리고 달성 여부는 비어 있을 수 없습니다"; + + @DisplayName("올바른 형식으로 TODO를 생성 시 예외가 발생하지 않는다") + @Test + void createValidTodo() { + assertThatCode(() -> new TravelPlaceTodo(VALID_PLACE, VALID_CONTENT, VALID_ORDER, VALID_CHECK)) + .doesNotThrowAnyException(); + } + + @DisplayName("TODO가 속하는 여행 계획 장소가 비어 있을 경우 TODO 생성 시 예외가 발생한다") + @Test + void createTodoWithNullPlace() { + assertThatThrownBy(() -> new TravelPlaceTodo(null, VALID_CONTENT, VALID_ORDER, VALID_CHECK)) + .isInstanceOf(BadRequestException.class) + .hasMessage(EMPTY_ERROR_MESSAGE); + } + + @DisplayName("TODO의 내용이 비어 있을 경우 TODO 생성 시 예외가 발생한다") + @Test + void createTodoWithNullContent() { + assertThatThrownBy(() -> new TravelPlaceTodo(VALID_PLACE, null, VALID_ORDER, VALID_CHECK)) + .isInstanceOf(BadRequestException.class) + .hasMessage(EMPTY_ERROR_MESSAGE); + } + + @DisplayName("TODO의 순서가 비어 있을 경우 TODO 생성 시 예외가 발생한다") + @Test + void createTodoWithNullOrder() { + assertThatThrownBy(() -> new TravelPlaceTodo(VALID_PLACE, VALID_CONTENT, null, VALID_CHECK)) + .isInstanceOf(BadRequestException.class) + .hasMessage(EMPTY_ERROR_MESSAGE); + } + + @DisplayName("TODO의 체크 여부가 비어 있을 경우 TODO 생성 시 예외가 발생한다") + @Test + void createTodoWithNullCheck() { + assertThatThrownBy(() -> new TravelPlaceTodo(VALID_PLACE, VALID_CONTENT, VALID_ORDER, null)) + .isInstanceOf(BadRequestException.class) + .hasMessage(EMPTY_ERROR_MESSAGE); + } + + @DisplayName("TODO의 내용이 공백인 경우 TODO 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(strings = {" ", " ", "\n"}) + void createTodoWithBlankContent(String blank) { + assertThatThrownBy(() -> new TravelPlaceTodo(VALID_PLACE, blank, VALID_ORDER, VALID_CHECK)) + .isInstanceOf(BadRequestException.class) + .hasMessage("TODO 내용은 빈 문자열로만 이루어질 수 없습니다"); + } + + @DisplayName("TODO의 내용 길이가 범위를 벗어나면 TODO 생성 시 예외가 발생한다") + @Test + void createTodoWithInvalidLengthContent() { + String length21 = "서울 명동에서 패션 쇼핑과 길거리 음식"; + System.out.println("length21 = " + length21.length()); + assertThatThrownBy(() -> new TravelPlaceTodo(VALID_PLACE, length21, VALID_ORDER, VALID_CHECK)) + .isInstanceOf(BadRequestException.class) + .hasMessage("TODO 내용의 길이는 1자 이상, 20자 이하여야 합니다"); + } + + @DisplayName("TODO의 순서가 음수인 경우 TODO 생성 시 예외가 발생한다") + @ParameterizedTest + @ValueSource(ints = {-1, -2, -3, -4, -5}) + void createTodoWithNegativeOrder(int negative) { + assertThatThrownBy(() -> new TravelPlaceTodo(VALID_PLACE, VALID_CONTENT, negative, VALID_CHECK)) + .isInstanceOf(BadRequestException.class) + .hasMessage("TODO 순서는 음수일 수 없습니다"); + } + + @DisplayName("TODO의 상태를 업데이트 할 수 있다") + @Test + void updateCheckStatus() { + TravelPlaceTodo todo = new TravelPlaceTodo(VALID_PLACE, VALID_CONTENT, VALID_ORDER, VALID_CHECK); + todo.updateCheckedStatus(false); + + assertThat(todo.getIsChecked()).isFalse(); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java index e7c1a392..776606c6 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanPlaceTest.java @@ -15,7 +15,6 @@ @DisplayName("여행 계획 장소") class TravelPlanPlaceTest { - private static final String VALID_DESCRIPTION = "함덕에서 유명한 맛집"; private static final Integer VALID_ORDER = 0; private static final TravelPlanDay VALID_DAY = TravelPlanDayFixture.TRAVEL_PLAN_DAY.get(); private static final Place VALID_PLACE = PlaceFixture.PLACE.get(); @@ -23,21 +22,14 @@ class TravelPlanPlaceTest { @DisplayName("올바른 여행 계획 장소 생성 시 예외가 발생하지 않는다") @Test void createTravelPlanPlaceWithValidData() { - assertThatCode(() -> new TravelPlanPlace(VALID_DESCRIPTION, VALID_ORDER, VALID_DAY, VALID_PLACE)) - .doesNotThrowAnyException(); - } - - @DisplayName("여행 계획 장소의 설명이 비어 있어도 예외가 발생하지 않는다") - @Test - void createTravelPlanPlaceWithNullDescription() { - assertThatCode(() -> new TravelPlanPlace(null, VALID_ORDER, VALID_DAY, VALID_PLACE)) + assertThatCode(() -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, VALID_PLACE)) .doesNotThrowAnyException(); } @DisplayName("방문 순서가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") @Test void createTravelPlanPlaceWithNullOrder() { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, null, VALID_DAY, VALID_PLACE)) + assertThatThrownBy(() -> new TravelPlanPlace(null, VALID_DAY, VALID_PLACE)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); } @@ -45,7 +37,7 @@ void createTravelPlanPlaceWithNullOrder() { @DisplayName("장소의 방문 날짜가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") @Test void createTravelPlanPlaceWithNullDay() { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, VALID_ORDER, null, VALID_PLACE)) + assertThatThrownBy(() -> new TravelPlanPlace(VALID_ORDER, null, VALID_PLACE)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); } @@ -53,26 +45,16 @@ void createTravelPlanPlaceWithNullDay() { @DisplayName("장소 상세 정보가 비어 있을 경우 여행 계획 장소 생성 시 예외가 발생한다") @Test void createTravelPlanPlaceWithPlaceNull() { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, VALID_ORDER, VALID_DAY, null)) + assertThatThrownBy(() -> new TravelPlanPlace(VALID_ORDER, VALID_DAY, null)) .isInstanceOf(BadRequestException.class) .hasMessage("여행 계획 장소에서 순서와 날짜, 그리고 장소 상세는 비어 있을 수 없습니다"); } - @DisplayName("여행 계획 장소 설명이 300자를 넘는 경우 여행 계획 장소 생성 시 예외가 발생한다") - @Test - void createTravelPlanPlaceWithTitleLengthOver300() { - String length301 = "함덕 해수욕장은 제주도의 아름다운 해변으로, 맑고 푸른 바다와 깨끗한 백사장이 특징입니다. 이곳은 특히 여름철에 인기가 높으며, 가족 단위 방문객과 커플 모두에게 적합한 장소입니다. 해변의 파도는 비교적 잔잔하여 수영이나 서핑을 즐기기에 좋고, 주변에는 다양한 해산물 맛집과 카페들이 있어 식사와 음료를 즐기기에 편리합니다. 해변을 따라 펼쳐진 산책로에서는 제주도의 자연경관을 감상하며 여유로운 산책이 가능합니다. 특히 일몰 시의 경치가 아름다워 사진 찍기에도 최적의 장소입니다. 함덕 해수욕장에서 제주도의 매력을 만끽해 보세요!!!!!!"; - - assertThatThrownBy(() -> new TravelPlanPlace(length301, VALID_ORDER, VALID_DAY, VALID_PLACE)) - .isInstanceOf(BadRequestException.class) - .hasMessage("장소 설명은 300자를 넘을 수 없습니다"); - } - @DisplayName("여행 계획 장소의 방문 순서가 음수인 경우 여행 계획 장소 생성 시 예외가 발생한다") @ParameterizedTest @ValueSource(ints = {-1, -2, -3, -4, -5}) void createTravelPlanPlaceWithNegativeOrder(int negative) { - assertThatThrownBy(() -> new TravelPlanPlace(VALID_DESCRIPTION, negative, VALID_DAY, VALID_PLACE)) + assertThatThrownBy(() -> new TravelPlanPlace(negative, VALID_DAY, VALID_PLACE)) .isInstanceOf(BadRequestException.class) .hasMessage("장소의 방문 순서는 음수일 수 없습니다"); } diff --git a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java index 166c05a1..6d974348 100644 --- a/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/domain/TravelPlanTest.java @@ -1,6 +1,5 @@ package kr.touroot.travelplan.domain; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -11,6 +10,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -19,7 +19,7 @@ @DisplayName("여행 계획") class TravelPlanTest { - private static final Member VALID_AUTHOR = MEMBER_KAKAO.getMember(); + private static final Member VALID_AUTHOR = MemberFixture.KAKAO_MEMBER.build(); private static final String VALID_TITLE = "제주도 여행 계획"; private static final UUID VALID_UUID = UUID.randomUUID(); private static final LocalDate VALID_START_DATE = LocalDate.now().plusDays(2); diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java index c1952717..9a35c57b 100644 --- a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanFixture.java @@ -1,17 +1,17 @@ package kr.touroot.travelplan.fixture; -import static kr.touroot.authentication.fixture.MemberFixture.MEMBER_KAKAO; import java.time.LocalDate; import java.util.UUID; import kr.touroot.member.domain.Member; +import kr.touroot.member.fixture.MemberFixture; import kr.touroot.travelplan.domain.TravelPlan; import lombok.AllArgsConstructor; @AllArgsConstructor public enum TravelPlanFixture { - TRAVEL_PLAN("제주도 여행 계획", LocalDate.now().plusDays(2), MEMBER_KAKAO.getMember()); + TRAVEL_PLAN("제주도 여행 계획", LocalDate.now().plusDays(2), MemberFixture.KAKAO_MEMBER.build()); private final String title; private final LocalDate startDate; diff --git a/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java new file mode 100644 index 00000000..6a85b331 --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/fixture/TravelPlanPlaceFixture.java @@ -0,0 +1,27 @@ +package kr.touroot.travelplan.fixture; + +import kr.touroot.place.domain.Place; +import kr.touroot.place.fixture.PlaceFixture; +import kr.touroot.travelplan.domain.TravelPlanDay; +import kr.touroot.travelplan.domain.TravelPlanPlace; + +public enum TravelPlanPlaceFixture { + + TRAVEL_PLAN_PLACE("함덕 해수욕장 조아요!", 0, TravelPlanDayFixture.TRAVEL_PLAN_DAY.get(), PlaceFixture.PLACE.get()); + + private final String description; + private final Integer order; + private final TravelPlanDay travelPlanDay; + private final Place place; + + TravelPlanPlaceFixture(String description, Integer order, TravelPlanDay travelPlanDay, Place place) { + this.description = description; + this.order = order; + this.travelPlanDay = travelPlanDay; + this.place = place; + } + + public TravelPlanPlace get() { + return new TravelPlanPlace(order, travelPlanDay, place); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java index 27b919f1..23635823 100644 --- a/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelplan/helper/TravelPlanTestHelper.java @@ -7,9 +7,11 @@ import kr.touroot.member.repository.MemberRepository; import kr.touroot.place.domain.Place; import kr.touroot.place.repository.PlaceRepository; +import kr.touroot.travelplan.domain.TravelPlaceTodo; import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.domain.TravelPlanDay; import kr.touroot.travelplan.domain.TravelPlanPlace; +import kr.touroot.travelplan.repository.PlaceTodoRepository; import kr.touroot.travelplan.repository.TravelPlanDayRepository; import kr.touroot.travelplan.repository.TravelPlanPlaceRepository; import kr.touroot.travelplan.repository.TravelPlanRepository; @@ -24,6 +26,7 @@ public class TravelPlanTestHelper { private final TravelPlanDayRepository travelPlanDayRepository; private final TravelPlanPlaceRepository travelPlanPlaceRepository; private final MemberRepository memberRepository; + private final PlaceTodoRepository placeTodoRepository; @Autowired public TravelPlanTestHelper( @@ -31,13 +34,15 @@ public TravelPlanTestHelper( TravelPlanRepository travelPlanRepository, TravelPlanDayRepository travelPlanDayRepository, TravelPlanPlaceRepository travelPlanPlaceRepository, - MemberRepository memberRepository + MemberRepository memberRepository, + PlaceTodoRepository placeTodoRepository ) { this.placeRepository = placeRepository; this.travelPlanRepository = travelPlanRepository; this.travelPlanDayRepository = travelPlanDayRepository; this.travelPlanPlaceRepository = travelPlanPlaceRepository; this.memberRepository = memberRepository; + this.placeTodoRepository = placeTodoRepository; } public static Member getKakaoMember(Long kakaoId, String nickname, String profileImageUri) { @@ -56,21 +61,29 @@ public static TravelPlanDay getTravelPlanDay(int order, TravelPlan travelPlan) { return new TravelPlanDay(order, travelPlan); } - public static TravelPlanPlace getTravelPlanPlace(String description, int order, Place place, TravelPlanDay day) { - return new TravelPlanPlace(description, order, day, place); + public static TravelPlanPlace getTravelPlanPlace(int order, Place place, TravelPlanDay day) { + return new TravelPlanPlace(order, day, place); } + public static TravelPlaceTodo getTravelPlaceTodo(TravelPlanPlace travelPlanPlace, String content, Integer order, + Boolean isChecked) { + return new TravelPlaceTodo(travelPlanPlace, content, order, isChecked); + } + + public TravelPlan initTravelPlanTestData() { Member author = initMemberTestData(); TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); Place place = getPlace("장소", "37.5175896", "127.0867236", ""); - TravelPlanPlace travelPlanPlace = getTravelPlanPlace("설명", 0, place, travelPlanDay); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace(0, place, travelPlanDay); + TravelPlaceTodo travelPlaceTodo = getTravelPlaceTodo(travelPlanPlace, "테스트짜기", 0, false); travelPlanRepository.save(travelPlan); travelPlanDayRepository.save(travelPlanDay); placeRepository.save(place); travelPlanPlaceRepository.save(travelPlanPlace); + placeTodoRepository.save(travelPlaceTodo); return travelPlan; } @@ -79,12 +92,14 @@ public TravelPlan initTravelPlanTestData(Member author) { TravelPlan travelPlan = getTravelPlan("여행계획", LocalDate.MAX, author); TravelPlanDay travelPlanDay = getTravelPlanDay(0, travelPlan); Place place = getPlace("장소", "37.5175896", "127.0867236", ""); - TravelPlanPlace travelPlanPlace = getTravelPlanPlace("설명", 0, place, travelPlanDay); + TravelPlanPlace travelPlanPlace = getTravelPlanPlace(0, place, travelPlanDay); + TravelPlaceTodo travelPlaceTodo = getTravelPlaceTodo(travelPlanPlace, "테스트짜기", 0, false); travelPlanRepository.save(travelPlan); travelPlanDayRepository.save(travelPlanDay); placeRepository.save(place); travelPlanPlaceRepository.save(travelPlanPlace); + placeTodoRepository.save(travelPlaceTodo); return travelPlan; } diff --git a/backend/src/test/java/kr/touroot/travelplan/service/PlaceTodoServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/PlaceTodoServiceTest.java new file mode 100644 index 00000000..1ec7800d --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelplan/service/PlaceTodoServiceTest.java @@ -0,0 +1,85 @@ +package kr.touroot.travelplan.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import kr.touroot.global.ServiceTest; +import kr.touroot.global.auth.dto.MemberAuth; +import kr.touroot.global.config.TestQueryDslConfig; +import kr.touroot.global.exception.BadRequestException; +import kr.touroot.global.exception.ForbiddenException; +import kr.touroot.member.domain.Member; +import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.dto.request.TodoStatusUpdateRequest; +import kr.touroot.travelplan.dto.response.PlanPlaceTodoResponse; +import kr.touroot.travelplan.helper.TravelPlanTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.context.annotation.Import; + +@DisplayName("TODO 서비스") +@Import({PlaceTodoService.class, TravelPlanTestHelper.class, TestQueryDslConfig.class}) +@ServiceTest +class PlaceTodoServiceTest { + + private final PlaceTodoService placeTodoService; + private final DatabaseCleaner databaseCleaner; + private final TravelPlanTestHelper testHelper; + + private MemberAuth memberAuth; + private Member author; + + @Autowired + public PlaceTodoServiceTest( + PlaceTodoService placeTodoService, + DatabaseCleaner databaseCleaner, + TravelPlanTestHelper testHelper + ) { + this.placeTodoService = placeTodoService; + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + author = testHelper.initMemberTestData(); + memberAuth = new MemberAuth(author.getId()); + } + + @DisplayName("저장되어 있는 TODO의 체크 상태를 변경할 수 있다") + @Test + void updateTodoCheckStatus() { + TravelPlan savedPlan = testHelper.initTravelPlanTestData(author); + TodoStatusUpdateRequest updateRequest = new TodoStatusUpdateRequest(true); + + PlanPlaceTodoResponse updateTodoResponse = placeTodoService.updateTodoStatus(1L, memberAuth, updateRequest); + + assertThat(updateTodoResponse.checked()).isTrue(); + } + + @DisplayName("존재하지 않는 TODO의 체크 상태를 업데이트 하려고 할 경우 예외가 발생한다") + @Test + void updateNonExistTodoCheckStatus() { + TodoStatusUpdateRequest updateRequest = new TodoStatusUpdateRequest(true); + assertThatThrownBy(() -> placeTodoService.updateTodoStatus(1L, memberAuth, updateRequest)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 TODO 입니다"); + } + + @DisplayName("작성자가 아닌 멤버가 TODO를 업데이트하려고 하는 경우 예외가 발생한다") + @Test + void updateTodoCheckStatusFromNonAuthor() { + TravelPlan savedPlan = testHelper.initTravelPlanTestData(author); + Member notAuthor = testHelper.initMemberTestData(); + MemberAuth nonAuthorAccessor = new MemberAuth(notAuthor.getId()); + + TodoStatusUpdateRequest updateRequest = new TodoStatusUpdateRequest(true); + assertThatThrownBy(() -> placeTodoService.updateTodoStatus(1L, nonAuthorAccessor, updateRequest)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("TODO 체크는 작성자만 가능합니다"); + } +} diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index 2d17fb2f..d71769f3 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; +import java.util.Collections; import java.util.List; import java.util.UUID; import kr.touroot.global.ServiceTest; @@ -13,12 +14,12 @@ import kr.touroot.global.exception.ForbiddenException; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; +import kr.touroot.travelplan.dto.request.PlanCreateRequest; import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; -import kr.touroot.travelplan.dto.request.TravelPlanCreateRequest; -import kr.touroot.travelplan.dto.response.TravelPlanCreateResponse; -import kr.touroot.travelplan.dto.response.TravelPlanResponse; +import kr.touroot.travelplan.dto.response.PlanCreateResponse; +import kr.touroot.travelplan.dto.response.PlanResponse; import kr.touroot.travelplan.helper.TravelPlanTestHelper; import kr.touroot.travelplan.repository.TravelPlanRepository; import kr.touroot.utils.DatabaseCleaner; @@ -69,18 +70,18 @@ void createTravelPlan() { PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") - .description("신나는 여행 장소") + .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + PlanCreateRequest request = PlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MAX) .days(List.of(planDayCreateRequest)) .build(); // when - TravelPlanCreateResponse actual = travelPlanService.createTravelPlan(request, memberAuth); + PlanCreateResponse actual = travelPlanService.createTravelPlan(request, memberAuth); // then assertThat(actual.id()).isEqualTo(1L); @@ -93,11 +94,11 @@ void createTravelPlanWithInvalidStartDate() { PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") - .description("신나는 여행 장소") .position(locationRequest) + .todos(Collections.EMPTY_LIST) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + PlanCreateRequest request = PlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MIN) .days(List.of(planDayCreateRequest)) @@ -116,11 +117,11 @@ void createTravelPlanStartsAtToday() { PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() .placeName("잠실한강공원") - .description("신나는 여행 장소") + .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - TravelPlanCreateRequest request = TravelPlanCreateRequest.builder() + PlanCreateRequest request = PlanCreateRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.now()) .days(List.of(planDayCreateRequest)) @@ -138,7 +139,7 @@ void readTravelPlan() { Long id = testHelper.initTravelPlanTestData(author).getId(); // when - TravelPlanResponse actual = travelPlanService.readTravelPlan(id, memberAuth); + PlanResponse actual = travelPlanService.readTravelPlan(id, memberAuth); // then assertThat(actual.id()).isEqualTo(id); @@ -226,7 +227,7 @@ void readTravelPlanByShareKey() { TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); // when - TravelPlanResponse actual = travelPlanService.readTravelPlan(travelPlan.getShareKey()); + PlanResponse actual = travelPlanService.readTravelPlan(travelPlan.getShareKey()); // then assertThat(actual.shareKey()).isEqualTo(travelPlan.getShareKey()); From 6f90028c85786432d8470debc68ae32f9f38d3fd Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Fri, 16 Aug 2024 17:09:31 +0900 Subject: [PATCH 098/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 수정 기능 구현 * refactor: 불필요한 DTO 제거 * refactor: 불필요한 DTO 제거 * refactor: 생성 시각 컬럼 null 및 수정 금지 * feat: 제목 및 썸네일 수정자 추가 * feat: 여행기 수정을 위한 기존 데이터 제거 로직 추가 * feat: 여행기 수정 기능 추가 * test: 여행기 수정 기능 테스트 작성 * refactor: 여행기 수정 API 문서화 수정 * refactor: 여행기 수정 API가 수정 대상 엔티티 ID를 path variable로 받도록 개선 * refactor: 여행기 수정 기능 쿼리 및 테스트 개선 * refactor: 여행기 하위 서비스 Transactional 어노테이션 추가 * refactor: 여행기 관련 repository 누락된 bulk 삭제 반영 --- .../kr/touroot/global/entity/BaseEntity.java | 2 + .../controller/TravelogueController.java | 27 ++++++ .../touroot/travelogue/domain/Travelogue.java | 5 + .../travelogue/domain/TraveloguePlace.java | 2 +- .../repository/TravelogueDayRepository.java | 6 +- .../repository/TraveloguePhotoRepository.java | 6 +- .../repository/TraveloguePlaceRepository.java | 6 +- .../repository/TravelogueTagRepository.java | 6 ++ .../service/TravelogueDayService.java | 7 +- .../service/TravelogueFacadeService.java | 25 ++++- .../service/TraveloguePhotoService.java | 6 +- .../service/TraveloguePlaceService.java | 7 +- .../travelogue/service/TravelogueService.java | 24 ++++- .../service/TravelogueTagService.java | 15 +++ .../controller/TravelogueControllerTest.java | 97 +++++++++++++++++-- .../travelogue/domain/TravelogueTest.java | 16 +++ .../fixture/TravelogueRequestFixture.java | 22 +++++ .../fixture/TravelogueResponseFixture.java | 48 +++++++++ .../helper/TravelogueTestHelper.java | 19 ++++ .../service/TravelogueDayServiceTest.java | 4 +- .../service/TravelogueFacadeServiceTest.java | 85 +++++++++++++--- .../service/TraveloguePhotoServiceTest.java | 2 +- .../service/TraveloguePlaceServiceTest.java | 2 +- .../service/TravelogueServiceTest.java | 66 ++++++++++++- 24 files changed, 458 insertions(+), 47 deletions(-) diff --git a/backend/src/main/java/kr/touroot/global/entity/BaseEntity.java b/backend/src/main/java/kr/touroot/global/entity/BaseEntity.java index 8a57dc9a..4e8b0238 100644 --- a/backend/src/main/java/kr/touroot/global/entity/BaseEntity.java +++ b/backend/src/main/java/kr/touroot/global/entity/BaseEntity.java @@ -1,5 +1,6 @@ package kr.touroot.global.entity; +import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; @@ -14,6 +15,7 @@ public abstract class BaseEntity { @CreatedDate + @Column(updatable = false, nullable = false) private LocalDateTime createdAt; @LastModifiedDate diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index 2e795bed..54280d50 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -125,6 +126,32 @@ public ResponseEntity> findTraveloguesByKeyword( return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(pageable, searchRequest)); } + @Operation(summary = "여행기 수정") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 값이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "작성자가 아닌 사용자가 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PutMapping("/{id}") + public ResponseEntity updateTravelogue( + @PathVariable Long id, + @Valid MemberAuth member, + @Valid @RequestBody TravelogueRequest request + ) { + return ResponseEntity.ok(travelogueFacadeService.updateTravelogue(id, member, request)); + } + @Operation(summary = "여행기 삭제") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java index b4c2c95b..1a03b001 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/Travelogue.java @@ -57,6 +57,11 @@ public Travelogue(Member author, String title, String thumbnail) { this(null, author, title, thumbnail); } + public void update(String title, String thumbnail) { + this.title = title; + this.thumbnail = thumbnail; + } + private void validate(Member author, String title, String thumbnail) { validateNotNull(author, title, thumbnail); validateNotBlank(title, thumbnail); diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java index a8135fc6..f88fb406 100644 --- a/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TraveloguePlace.java @@ -46,7 +46,7 @@ public class TraveloguePlace extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private TravelogueDay travelogueDay; - private TraveloguePlace(Long id, Integer order, String description, Place place, TravelogueDay travelogueDay) { + public TraveloguePlace(Long id, Integer order, String description, Place place, TravelogueDay travelogueDay) { validate(order, description, place, travelogueDay); this.id = id; this.order = order; diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java index 0e492f58..bcc5f0c3 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java @@ -4,10 +4,14 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface TravelogueDayRepository extends JpaRepository { List findByTravelogue(Travelogue travelogue); - void deleteByTravelogue(Travelogue travelogue); + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE TravelogueDay td SET td.deletedAt = NOW() WHERE td.travelogue = :travelogue") + void deleteAllByTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java index 16de240b..d25b3c79 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -5,10 +5,14 @@ import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface TraveloguePhotoRepository extends JpaRepository { List findByTraveloguePlace(TraveloguePlace traveloguePlace); - void deleteByTraveloguePlaceTravelogueDayTravelogue(Travelogue travelogue); + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE TraveloguePhoto tp SET tp.deletedAt = NOW() WHERE tp.traveloguePlace.travelogueDay.travelogue = :travelogue") + void deleteAllByTraveloguePlaceTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java index 49de742f..3f06bd06 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java @@ -5,10 +5,14 @@ import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePlace; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface TraveloguePlaceRepository extends JpaRepository { List findByTravelogueDay(TravelogueDay travelogueDay); - void deleteByTravelogueDayTravelogue(Travelogue travelogue); + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE TraveloguePlace tp SET tp.deletedAt = NOW() WHERE tp.travelogueDay.travelogue = :travelogue") + void deleteAllByTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java index 2f3f6fb7..d36ed1fc 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java @@ -4,8 +4,14 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueTag; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; public interface TravelogueTagRepository extends JpaRepository { List findAllByTravelogue(Travelogue travelogue); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("DELETE TravelogueTag tt WHERE tt.travelogue = :travelogue") + void deleteAllByTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java index f1b5957c..146c70d1 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java @@ -19,6 +19,7 @@ public class TravelogueDayService { private final TravelogueDayRepository travelogueDayRepository; + @Transactional public Map> createDays( List requests, Travelogue travelogue @@ -34,17 +35,19 @@ public Map> createDays( return daysWithPlaceRequests; } + @Transactional(readOnly = true) public List findDaysByTravelogue(Travelogue travelogue) { return travelogueDayRepository.findByTravelogue(travelogue); } + @Transactional(readOnly = true) public TravelogueDay findDayById(Long id) { return travelogueDayRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기 일자입니다.")); } @Transactional - public void deleteByTravelogue(Travelogue travelogue) { - travelogueDayRepository.deleteByTravelogue(travelogue); + public void deleteAllByTravelogue(Travelogue travelogue) { + travelogueDayRepository.deleteAllByTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index c5a91d85..d7fc3aeb 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -123,15 +123,32 @@ private TravelogueSimpleResponse getTravelogueSimpleResponse(Travelogue travelog return TravelogueSimpleResponse.of(travelogue, tagResponses); } + @Transactional + public TravelogueResponse updateTravelogue(Long id, MemberAuth member, TravelogueRequest request) { + Member author = memberService.getById(member.memberId()); + Travelogue travelogue = travelogueService.getTravelogueById(id); + + Travelogue updatedTravelogue = travelogueService.update(id, author, request); + List tags = travelogueTagService.updateTravelogueTags(travelogue, request.tags()); + + clearTravelogueContents(travelogue); + + return TravelogueResponse.of(updatedTravelogue, createDays(request.days(), updatedTravelogue), tags); + } + + private void clearTravelogueContents(Travelogue travelogue) { + traveloguePhotoService.deleteAllByTravelogue(travelogue); + traveloguePlaceService.deleteAllByTravelogue(travelogue); + travelogueDayService.deleteAllByTravelogue(travelogue); + } + @Transactional public void deleteTravelogueById(Long id, MemberAuth member) { Member author = memberService.getById(member.memberId()); Travelogue travelogue = travelogueService.getTravelogueById(id); - travelogueService.validateDeleteByAuthor(travelogue, author); + travelogueService.validateAuthor(travelogue, author); - traveloguePhotoService.deleteByTravelogue(travelogue); - traveloguePlaceService.deleteByTravelogue(travelogue); - travelogueDayService.deleteByTravelogue(travelogue); + clearTravelogueContents(travelogue); travelogueService.delete(travelogue, author); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java index 5a1f18d5..1ac3d17d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java @@ -20,6 +20,7 @@ public class TraveloguePhotoService { private final TraveloguePhotoRepository traveloguePhotoRepository; private final AwsS3Provider s3Provider; + @Transactional public List createPhotos(List requests, TraveloguePlace place) { List photos = new ArrayList<>(); @@ -33,6 +34,7 @@ public List createPhotos(List requests, return photos; } + @Transactional(readOnly = true) public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { List photos = traveloguePhotoRepository.findByTraveloguePlace(traveloguePlace); @@ -43,7 +45,7 @@ public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { } @Transactional - public void deleteByTravelogue(Travelogue travelogue) { - traveloguePhotoRepository.deleteByTraveloguePlaceTravelogueDayTravelogue(travelogue); + public void deleteAllByTravelogue(Travelogue travelogue) { + traveloguePhotoRepository.deleteAllByTraveloguePlaceTravelogueDayTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java index 41c1ddc6..ca7f2951 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java @@ -23,6 +23,7 @@ public class TraveloguePlaceService { private final PlaceRepository placeRepository; private final TraveloguePlaceRepository traveloguePlaceRepository; + @Transactional public Map> createPlaces( List requests, TravelogueDay day @@ -48,17 +49,19 @@ private Place getPlace(TraveloguePlaceRequest request) { ).orElseGet(() -> placeRepository.save(request.toPlace())); } + @Transactional(readOnly = true) public List findTraveloguePlacesByDay(TravelogueDay travelogueDay) { return traveloguePlaceRepository.findByTravelogueDay(travelogueDay); } + @Transactional(readOnly = true) public TraveloguePlace findTraveloguePlaceById(Long id) { return traveloguePlaceRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기 장소입니다.")); } @Transactional - public void deleteByTravelogue(Travelogue travelogue) { - traveloguePlaceRepository.deleteByTravelogueDayTravelogue(travelogue); + public void deleteAllByTravelogue(Travelogue travelogue) { + traveloguePlaceRepository.deleteAllByTravelogueDayTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index 300e269d..f617ceea 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -21,37 +22,54 @@ public class TravelogueService { private final AwsS3Provider s3Provider; private final TravelogueQueryRepository travelogueQueryRepository; + @Transactional public Travelogue createTravelogue(Member author, TravelogueRequest request) { String url = s3Provider.copyImageToPermanentStorage(request.thumbnail()); Travelogue travelogue = request.toTravelogueOf(author, url); return travelogueRepository.save(travelogue); } + @Transactional(readOnly = true) public Travelogue getTravelogueById(Long id) { return travelogueRepository.findById(id) .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기입니다.")); } + @Transactional(readOnly = true) public Page findAll(Pageable pageable) { return travelogueRepository.findAll(pageable); } + @Transactional(readOnly = true) public Page findAllByMember(Member member, Pageable pageable) { return travelogueRepository.findAllByAuthor(member, pageable); } + @Transactional(readOnly = true) public Page findByKeyword(String keyword, Pageable pageable) { return travelogueQueryRepository.findByTitleContaining(keyword, pageable); } + @Transactional + public Travelogue update(Long id, Member author, TravelogueRequest request) { + Travelogue travelogue = travelogueRepository.findById(id) + .orElseThrow(() -> new BadRequestException("존재하지 않는 여행기입니다.")); + validateAuthor(travelogue, author); + + travelogue.update(request.title(), request.thumbnail()); + + return travelogueRepository.save(travelogue); + } + + @Transactional public void delete(Travelogue travelogue, Member author) { - validateDeleteByAuthor(travelogue, author); + validateAuthor(travelogue, author); travelogueRepository.delete(travelogue); } - public void validateDeleteByAuthor(Travelogue travelogue, Member author) { + public void validateAuthor(Travelogue travelogue, Member author) { if (!travelogue.isAuthor(author)) { - throw new ForbiddenException("여행기 삭제는 작성자만 가능합니다."); + throw new ForbiddenException("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다."); } } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java index 901c6d8a..7b441eca 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java @@ -10,6 +10,7 @@ import kr.touroot.travelogue.repository.TravelogueTagRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -18,6 +19,7 @@ public class TravelogueTagService { private final TagRepository tagRepository; private final TravelogueTagRepository travelogueTagRepository; + @Transactional public List createTravelogueTags(Travelogue travelogue, List tagIds) { return tagIds.stream() .map(id -> { @@ -32,9 +34,22 @@ private Tag getTagById(Long id) { .orElseThrow(() -> new BadRequestException("존재하지 않는 태그입니다.")); } + @Transactional(readOnly = true) public List readTagByTravelogue(Travelogue travelogue) { return travelogueTagRepository.findAllByTravelogue(travelogue).stream() .map(travelogueTag -> TagResponse.from(travelogueTag.getTag())) .toList(); } + + @Transactional(readOnly = true) + public List updateTravelogueTags(Travelogue travelogue, List tagIds) { + travelogueTagRepository.deleteAllByTravelogue(travelogue); + + return tagIds.stream() + .map(id -> { + Tag tag = getTagById(id); + travelogueTagRepository.save(new TravelogueTag(travelogue, tag)); + return TagResponse.from(tag); + }).toList(); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 6ffca5d1..363bd7fe 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -85,9 +86,7 @@ void createTravelogue() { Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); - List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); - List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); - List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + List days = getTravelogueDayRequests(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); RestAssured.given().log().all() @@ -100,6 +99,12 @@ void createTravelogue() { .header("Location", "/api/v1/travelogues/1"); } + private List getTravelogueDayRequests() { + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + return TravelogueRequestFixture.getTravelogueDayRequests(places); + } + @DisplayName("태그가 있는 여행기를 작성한다.") @Test void createTravelogueWithTags() { @@ -108,9 +113,7 @@ void createTravelogueWithTags() { testHelper.initTagTestData(); - List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); - List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); - List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + List days = getTravelogueDayRequests(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days, List.of(1L)); RestAssured.given().log().all() @@ -193,9 +196,7 @@ void createTravelogueWithNoPlacesDay() throws JsonProcessingException { @DisplayName("여행기를 작성할 때 로그인 되어 있지 않으면 예외가 발생한다.") @Test void createTravelogueWithNotLoginThrowException() { - List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); - List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); - List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + List days = getTravelogueDayRequests(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); RestAssured.given().log().all() @@ -306,6 +307,82 @@ void findTraveloguesKeywordWithMiddleBlank(String keyword) throws JsonProcessing .body(is(objectMapper.writeValueAsString(responses))); } + @DisplayName("여행기를 수정한다.") + @Test + void updateTravelogue() throws JsonProcessingException { + Travelogue travelogue = testHelper.initTravelogueTestData(member); + + List days = getUpdateTravelogueDayRequests(); + saveImages(days); + + TravelogueRequest request = TravelogueRequestFixture.getUpdateTravelogueRequest(days); + TravelogueResponse response = TravelogueResponseFixture.getUpdatedTravelogueResponse(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().put("/api/v1/travelogues/" + travelogue.getId()) + .then().log().all() + .statusCode(200) + .body(is(objectMapper.writeValueAsString(response))); + } + + private List getUpdateTravelogueDayRequests() { + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getUpdateTraveloguePlaceRequests(photos); + return TravelogueRequestFixture.getUpdateTravelogueDayRequests(places); + } + + private void saveImages(List days) { + when(s3Provider.copyImageToPermanentStorage( + TravelogueRequestFixture.getTravelogueRequest(days).thumbnail()) + ).thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + when(s3Provider.copyImageToPermanentStorage( + TravelogueRequestFixture.getTraveloguePhotoRequests().get(0).url()) + ).thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); + } + + @DisplayName("존재하지 않는 여행기를 수정 시, 400을 응답한다.") + @Test + void updateTravelogueWithNotExist() { + testHelper.initTravelogueTestData(member); + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + List days = getTravelogueDayRequests(); + TravelogueRequest request = TravelogueRequestFixture.getUpdateTravelogueRequest(days); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().put("/api/v1/travelogues/" + 0) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행기입니다.")); + } + + @DisplayName("작성자가 아닌 사용자가 여행기 수정 시 403을 응답한다.") + @Test + void updateTravelogueWithNotAuthor() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + List days = getTravelogueDayRequests(); + TravelogueRequest request = TravelogueRequestFixture.getUpdateTravelogueRequest(days); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().put("/api/v1/travelogues/" + travelogue.getId()) + .then().log().all() + .statusCode(403) + .body("message", is("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다.")); + } + @DisplayName("여행기를 삭제한다.") @Test void deleteTravelogue() { @@ -342,6 +419,6 @@ void deleteTravelogueWithNotAuthor() { .when().delete("/api/v1/travelogues/" + travelogue.getId()) .then().log().all() .statusCode(403) - .body("message", is("여행기 삭제는 작성자만 가능합니다.")); + .body("message", is("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다.")); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java index 1b036965..910335e4 100644 --- a/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/domain/TravelogueTest.java @@ -1,7 +1,9 @@ package kr.touroot.travelogue.domain; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; @@ -17,6 +19,20 @@ class TravelogueTest { private static final Member VALID_AUTHOR = MemberFixture.KAKAO_MEMBER.build(); private static final String VALID_TITLE = "올바른 여행기 제목"; private static final String VALID_THUMBNAIL = "http://valid-thumbnail.com"; + private static final String UPDATED_TITLE = "수정된 여행기 제목"; + private static final String UPDATED_THUMBNAIL = "http://updated-thumbnail.com"; + + @DisplayName("여행기를 수정한다.") + @Test + void update() { + Travelogue travelogue = new Travelogue(VALID_AUTHOR, VALID_TITLE, VALID_THUMBNAIL); + travelogue.update(UPDATED_TITLE, UPDATED_THUMBNAIL); + + assertAll( + () -> assertThat(travelogue.getTitle()).isEqualTo(UPDATED_TITLE), + () -> assertThat(travelogue.getThumbnail()).isEqualTo(UPDATED_THUMBNAIL) + ); + } @DisplayName("검증 규칙에 어긋나지 않는 여행기 생성 시 예외가 발생하지 않는다") @Test diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java index 0d749df0..e4987c74 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueRequestFixture.java @@ -32,10 +32,23 @@ public static TravelogueRequest getTravelogueRequest(List ); } + public static TravelogueRequest getUpdateTravelogueRequest(List days) { + return new TravelogueRequest( + "삼춘! 제주에 하영 옵서!", + "https://dev.touroot.kr/temporary/jeju_thumbnail_2.png", + List.of(), + days + ); + } + public static List getTravelogueDayRequests(List places) { return List.of(new TravelogueDayRequest(places)); } + public static List getUpdateTravelogueDayRequests(List places) { + return List.of(new TravelogueDayRequest(places), new TravelogueDayRequest(places)); + } + public static List getTraveloguePlaceRequests(List photos) { return List.of(new TraveloguePlaceRequest( "함덕해수욕장", @@ -45,6 +58,15 @@ public static List getTraveloguePlaceRequests(List getUpdateTraveloguePlaceRequests(List photos) { + return List.of(new TraveloguePlaceRequest( + "함덕해수욕장", + getTraveloguePositionRequest(), + "에메랄드 빛 해변은 해외 휴양지와 견줘도 밀리지 않습니다.", + photos + )); + } + public static TraveloguePositionRequest getTraveloguePositionRequest() { return new TraveloguePositionRequest("34.54343", "126.66977"); } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 1defa64a..2a4fbc60 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -35,6 +35,20 @@ public static TravelogueResponse getTravelogueResponse() { .build(); } + public static TravelogueResponse getUpdatedTravelogueResponse() { + return TravelogueResponse.builder() + .id(1L) + .title("삼춘! 제주에 하영 옵서!") + .createdAt(LocalDate.now()) + .authorNickname("리비") + .authorId(1L) + .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") + .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail_2.png") + .days(getUpdatedTravelogueDayResponses()) + .tags(List.of()) + .build(); + } + public static TravelogueResponse getTravelogueResponseWithTag() { return TravelogueResponse.builder() .id(1L) @@ -80,6 +94,18 @@ public static List getTravelogueDayResponses() { ); } + public static List getUpdatedTravelogueDayResponses() { + return List.of(TravelogueDayResponse.builder() + .id(2L) + .places(getUpdatedTraveloguePlaceResponses()) + .build(), + TravelogueDayResponse.builder() + .id(3L) + .places(getAddedTraveloguePlaceResponsesWhenUpdate()) + .build() + ); + } + public static List getTraveloguePlaceResponses() { return List.of(TraveloguePlaceResponse.builder() .id(1L) @@ -91,6 +117,28 @@ public static List getTraveloguePlaceResponses() { ); } + public static List getUpdatedTraveloguePlaceResponses() { + return List.of(TraveloguePlaceResponse.builder() + .id(2L) + .placeName("함덕해수욕장") + .description("에메랄드 빛 해변은 해외 휴양지와 견줘도 밀리지 않습니다.") + .position(getTraveloguePositionResponse()) + .photoUrls(getTraveloguePhotoUrls()) + .build() + ); + } + + public static List getAddedTraveloguePlaceResponsesWhenUpdate() { + return List.of(TraveloguePlaceResponse.builder() + .id(3L) + .placeName("함덕해수욕장") + .description("에메랄드 빛 해변은 해외 휴양지와 견줘도 밀리지 않습니다.") + .position(getTraveloguePositionResponse()) + .photoUrls(getTraveloguePhotoUrls()) + .build() + ); + } + public static TraveloguePositionResponse getTraveloguePositionResponse() { return TraveloguePositionResponse.builder() .id(1L) diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 7888d14b..30b4ebb9 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -6,6 +6,7 @@ import static kr.touroot.travelogue.fixture.TraveloguePhotoFixture.TRAVELOGUE_PHOTO; import static kr.touroot.travelogue.fixture.TraveloguePlaceFixture.TRAVELOGUE_PLACE; +import java.util.List; import kr.touroot.member.domain.LoginType; import kr.touroot.member.domain.Member; import kr.touroot.member.fixture.MemberFixture; @@ -72,6 +73,11 @@ public Travelogue initTravelogueTestData() { return initTravelogueTestData(author); } + public Travelogue initTravelogueTestDataWithSeveralDays() { + Member author = persistMember(); + return initTravelogueTestDataWithSeveralDays(author); + } + public Travelogue initTravelogueTestData(Member author) { Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); @@ -82,6 +88,19 @@ public Travelogue initTravelogueTestData(Member author) { return travelogue; } + public Travelogue initTravelogueTestDataWithSeveralDays(Member author) { + Travelogue travelogue = persistTravelogue(author); + List days = List.of(persistTravelogueDay(travelogue), persistTravelogueDay(travelogue)); + Place position = persistPlace(); + + days.stream() + .map(day -> persistTraveloguePlace(position, day)) + .map(this::persistTraveloguePhoto) + .toList(); + + return travelogue; + } + public Travelogue initTravelogueTestDataWithTag(Member author) { Travelogue travelogue = persistTravelogue(author); TravelogueDay day = persistTravelogueDay(travelogue); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java index 1e7cde22..27a589b3 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -102,9 +102,9 @@ void findDayByInvalidIdThrowException() { @DisplayName("주어진 여행기의 여행기 일자를 삭제할 수 있다.") @Test void deleteTravelogueDayById() { - Travelogue travelogue = testHelper.initTravelogueTestData(); + Travelogue travelogue = testHelper.initTravelogueTestDataWithSeveralDays(); long travelogueId = travelogue.getId(); - dayService.deleteByTravelogue(travelogue); + dayService.deleteAllByTravelogue(travelogue); assertThat(dayRepository.findAll() .stream() diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index b0e28929..91659a59 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -12,6 +12,7 @@ import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; +import kr.touroot.member.domain.Member; import kr.touroot.member.service.MemberService; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; @@ -77,18 +78,10 @@ void setUp() { @DisplayName("여행기를 생성할 수 있다.") @Test void createTravelogue() { - List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); - List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); - List days = TravelogueRequestFixture.getTravelogueDayRequests(places); - when(s3Provider.copyImageToPermanentStorage( - TravelogueRequestFixture.getTravelogueRequest(days).thumbnail()) - ).thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); - when(s3Provider.copyImageToPermanentStorage( - TravelogueRequestFixture.getTraveloguePhotoRequests().get(0).url()) - ).thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); + List days = getTravelogueDayRequests(); + saveImages(days); testHelper.initKakaoMemberTestData(); - MemberAuth memberAuth = new MemberAuth(1L); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); @@ -96,6 +89,21 @@ void createTravelogue() { .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } + private void saveImages(List days) { + when(s3Provider.copyImageToPermanentStorage( + TravelogueRequestFixture.getTravelogueRequest(days).thumbnail()) + ).thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + when(s3Provider.copyImageToPermanentStorage( + TravelogueRequestFixture.getTraveloguePhotoRequests().get(0).url()) + ).thenReturn(TravelogueResponseFixture.getTraveloguePhotoUrls().get(0)); + } + + private List getTravelogueDayRequests() { + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + return TravelogueRequestFixture.getTravelogueDayRequests(places); + } + @DisplayName("여행기를 ID를 기준으로 조회한다.") @Test void findTravelogueById() { @@ -130,6 +138,61 @@ void findTraveloguesByKeyword() { assertThat(searchResults).containsAll(responses); } + @DisplayName("여행기를 수정할 수 있다.") + @Test + void updateTravelogue() { + List days = getUpdateTravelogueDayRequests(); + saveImages(days); + + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(author); + + MemberAuth memberAuth = new MemberAuth(author.getId()); + TravelogueRequest request = TravelogueRequestFixture.getUpdateTravelogueRequest(days); + + assertThat(service.updateTravelogue(1L, memberAuth, request)) + .isEqualTo(TravelogueResponseFixture.getUpdatedTravelogueResponse()); + } + + private List getUpdateTravelogueDayRequests() { + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getUpdateTraveloguePlaceRequests(photos); + return TravelogueRequestFixture.getUpdateTravelogueDayRequests(places); + } + + @DisplayName("존재하지 않는 여행기를 수정하면 예외가 발생한다.") + @Test + void updateTravelogueWithNotExist() { + List days = getUpdateTravelogueDayRequests(); + saveImages(days); + + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(author); + + MemberAuth memberAuth = new MemberAuth(author.getId()); + TravelogueRequest request = TravelogueRequestFixture.getUpdateTravelogueRequest(days); + + assertThatThrownBy(() -> service.updateTravelogue(0L, memberAuth, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + + @DisplayName("작성자가 아닌 사용자가 여행기를 수정하면 예외가 발생한다.") + @Test + void updateByIdWithNotAuthor() { + testHelper.initTravelogueTestData(); + MemberAuth notAuthorAuth = new MemberAuth(testHelper.initKakaoMemberTestData().getId()); + + List days = getTravelogueDayRequests(); + saveImages(days); + + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); + + assertThatThrownBy(() -> service.updateTravelogue(1L, notAuthorAuth, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다."); + } + @DisplayName("여행기를 ID를 기준으로 삭제한다.") @Test void deleteById() { @@ -160,6 +223,6 @@ void deleteByIdWithNotAuthor() { assertThatThrownBy(() -> service.deleteTravelogueById(1L, notAuthorAuth)) .isInstanceOf(ForbiddenException.class) - .hasMessage("여행기 삭제는 작성자만 가능합니다."); + .hasMessage("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다."); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java index ba2de820..230fff22 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -95,7 +95,7 @@ void findPhotoUrlsByPlace() { void deleteTraveloguePhotoById() { Travelogue travelogue = testHelper.initTravelogueTestData(); long travelogueId = travelogue.getId(); - photoService.deleteByTravelogue(travelogue); + photoService.deleteAllByTravelogue(travelogue); assertThat(photoRepository.findAll() .stream() diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 60e01d75..4f5c8f12 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -107,7 +107,7 @@ void findDayByInvalidIdThrowException() { void deleteTraveloguePlaceById() { Travelogue travelogue = testHelper.initTravelogueTestData(); long travelogueId = travelogue.getId(); - placeService.deleteByTravelogue(travelogue); + placeService.deleteAllByTravelogue(travelogue); assertThat(placeRepository.findAll() .stream() diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java index 0ceb7f84..e9ac1b08 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueServiceTest.java @@ -69,9 +69,7 @@ void createTravelogue() { .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); Member author = testHelper.initKakaoMemberTestData(); - List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); - List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); - List days = TravelogueRequestFixture.getTravelogueDayRequests(places); + List days = getTravelogueDayRequests(); TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); Travelogue createdTravelogue = travelogueService.createTravelogue(author, request); @@ -82,6 +80,12 @@ void createTravelogue() { ); } + private static List getTravelogueDayRequests() { + List photos = TravelogueRequestFixture.getTraveloguePhotoRequests(); + List places = TravelogueRequestFixture.getTraveloguePlaceRequests(photos); + return TravelogueRequestFixture.getTravelogueDayRequests(places); + } + @DisplayName("여행기는 ID를 기준으로 조회할 수 있다.") @Test void getTravelogueById() { @@ -98,7 +102,6 @@ void getTravelogueByNotExistsIdThrowException() { .hasMessage("존재하지 않는 여행기입니다."); } - @DisplayName("여행기를 전체 조회할 수 있다.") @Test void findAll() { @@ -126,6 +129,59 @@ void findByKeywordWithNotExistKeyword() { .isEmpty(); } + @DisplayName("여행기를 수정할 수 있다.") + @Test + void updateTravelogue() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(author); + + List days = getTravelogueDayRequests(); + TravelogueRequest request = TravelogueRequestFixture.getUpdateTravelogueRequest(days); + Travelogue updatedTravelogue = travelogueService.update(1L, author, request); + + assertAll( + () -> assertThat(updatedTravelogue.getId()).isEqualTo(1L), + () -> assertThat(updatedTravelogue.getTitle()).isEqualTo("삼춘! 제주에 하영 옵서!") + ); + } + + @DisplayName("작성자가 아닌 사람이 여행기를 수정하면 예외가 발생한다.") + @Test + void updateTravelogueWithNotAuthor() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(); + + List days = getTravelogueDayRequests(); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); + + assertThatThrownBy(() -> travelogueService.update(1L, author, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다."); + } + + @DisplayName("존재하지 않는 여행기를 수정하면 예외가 발생한다.") + @Test + void updateTravelogueWithNotExist() { + Mockito.when(s3Provider.copyImageToPermanentStorage(any(String.class))) + .thenReturn(TravelogueResponseFixture.getTravelogueResponse().thumbnail()); + + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(author); + + List days = getTravelogueDayRequests(); + TravelogueRequest request = TravelogueRequestFixture.getTravelogueRequest(days); + + assertThatThrownBy(() -> travelogueService.update(0L, author, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + @DisplayName("여행기를 삭제할 수 있다.") @Test void deleteTravelogueById() { @@ -148,6 +204,6 @@ void deleteTravelogueByNotAuthorThrowException() { assertThatThrownBy(() -> travelogueService.delete(travelogue, notAuthor)) .isInstanceOf(ForbiddenException.class) - .hasMessage("여행기 삭제는 작성자만 가능합니다."); + .hasMessage("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다."); } } From af47c694334daf0626411777f4d6f95517ba4bce Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:32:57 +0900 Subject: [PATCH 099/108] =?UTF-8?q?[Feature]=20-=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=9C=EA=B7=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 메인 페이지 여행기 필터링 구현 * refactor: TravelogueController tagFilter query parameter tag-filter로 변경 --- .../controller/TravelogueController.java | 27 ++++++++++++++++++- .../TravelogueQueryRepository.java | 5 +++- .../TravelogueQueryRepositoryImpl.java | 23 +++++++++++++++- .../service/TravelogueFacadeService.java | 7 ++++- .../travelogue/service/TravelogueService.java | 8 +++++- .../global/config/TestQueryDslConfig.java | 4 +-- .../kr/touroot/tag/TagControllerTest.java | 2 +- .../kr/touroot/tag/fixture/TagFixture.java | 4 ++- .../kr/touroot/tag/helper/TagTestHelper.java | 2 +- .../touroot/tag/service/TagServiceTest.java | 2 +- .../controller/TravelogueControllerTest.java | 16 +++++++++++ .../fixture/TravelogueResponseFixture.java | 4 +-- .../helper/TravelogueTestHelper.java | 26 ++++++++++++++---- .../service/TravelogueFacadeServiceTest.java | 15 +++++++++++ 14 files changed, 127 insertions(+), 18 deletions(-) rename backend/src/main/java/kr/touroot/travelogue/repository/{ => query}/TravelogueQueryRepository.java (64%) rename backend/src/main/java/kr/touroot/travelogue/repository/{ => query}/TravelogueQueryRepositoryImpl.java (56%) diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index 54280d50..eb53a18d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.net.URI; +import java.util.List; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelogue.dto.request.TravelogueRequest; @@ -30,12 +31,13 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "여행기") @RequiredArgsConstructor @RestController -@RequestMapping("api/v1/travelogues") +@RequestMapping("/api/v1/travelogues") public class TravelogueController { private final TravelogueFacadeService travelogueFacadeService; @@ -102,6 +104,29 @@ public ResponseEntity> findMainPageTravelogues( return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(pageable)); } + @Operation(summary = "여행기 메인 페이지 필터링") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "올바르지 않은 페이지네이션 옵션으로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @PageableAsQueryParam + @GetMapping(params = {"tag-filter"}) + public ResponseEntity> findMainPageTravelogues( + @Parameter(hidden = true) + @PageableDefault(size = 5, sort = "id", direction = Direction.DESC) + Pageable pageable, + @RequestParam(name = "tag-filter", required = false) List tagFilter + ) { + return ResponseEntity.ok(travelogueFacadeService.findSimpleTravelogues(tagFilter, pageable)); + } + @Operation(summary = "여행기 검색") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java similarity index 64% rename from backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java rename to backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java index febbb4a5..fe575c9c 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepository.java @@ -1,5 +1,6 @@ -package kr.touroot.travelogue.repository; +package kr.touroot.travelogue.repository.query; +import java.util.List; import kr.touroot.travelogue.domain.Travelogue; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -7,4 +8,6 @@ public interface TravelogueQueryRepository { Page findByTitleContaining(String keyword, Pageable pageable); + + Page findAllByTag(List tagFilter, Pageable pageable); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java similarity index 56% rename from backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java rename to backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java index 3c940ef9..427cca74 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueQueryRepositoryImpl.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueQueryRepositoryImpl.java @@ -1,7 +1,9 @@ -package kr.touroot.travelogue.repository; +package kr.touroot.travelogue.repository.query; import static kr.touroot.travelogue.domain.QTravelogue.travelogue; +import static kr.touroot.travelogue.domain.QTravelogueTag.travelogueTag; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; @@ -30,4 +32,23 @@ public Page findByTitleContaining(String keyword, Pageable pageable) return new PageImpl<>(results, pageable, results.size()); } + + @Override + public Page findAllByTag(List tagFilter, Pageable pageable) { + List results = jpaQueryFactory.select(travelogue) + .from(travelogueTag) + .where(travelogueTag.tag.id.in(tagFilter)) + .groupBy(travelogueTag.travelogue) + .having(isSameCountWithFilter(tagFilter)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(results, pageable, results.size()); + } + + private BooleanExpression isSameCountWithFilter(List tagFilter) { + return travelogueTag.travelogue.count() + .eq(Long.valueOf(tagFilter.size())); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index d7fc3aeb..e1a61117 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -105,9 +105,14 @@ private List findPhotoUrlsOfTraveloguePlace(TraveloguePlace place) { } @Transactional(readOnly = true) - public Page findSimpleTravelogues(final Pageable pageable) { + public Page findSimpleTravelogues(Pageable pageable) { Page travelogues = travelogueService.findAll(pageable); + return travelogues.map(this::getTravelogueSimpleResponse); + } + @Transactional(readOnly = true) + public Page findSimpleTravelogues(List tagFilter, Pageable pageable) { + Page travelogues = travelogueService.findAllByFilter(tagFilter, pageable); return travelogues.map(this::getTravelogueSimpleResponse); } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java index f617ceea..1239b523 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueService.java @@ -1,12 +1,13 @@ package kr.touroot.travelogue.service; +import java.util.List; import kr.touroot.global.exception.BadRequestException; import kr.touroot.global.exception.ForbiddenException; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueRequest; -import kr.touroot.travelogue.repository.TravelogueQueryRepository; +import kr.touroot.travelogue.repository.query.TravelogueQueryRepository; import kr.touroot.travelogue.repository.TravelogueRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -50,6 +51,11 @@ public Page findByKeyword(String keyword, Pageable pageable) { return travelogueQueryRepository.findByTitleContaining(keyword, pageable); } + @Transactional(readOnly = true) + public Page findAllByFilter(List filter, Pageable pageable) { + return travelogueQueryRepository.findAllByTag(filter, pageable); + } + @Transactional public Travelogue update(Long id, Member author, TravelogueRequest request) { Travelogue travelogue = travelogueRepository.findById(id) diff --git a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java index 4be2bd08..838d7964 100644 --- a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java +++ b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java @@ -3,8 +3,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import kr.touroot.travelogue.repository.TravelogueQueryRepository; -import kr.touroot.travelogue.repository.TravelogueQueryRepositoryImpl; +import kr.touroot.travelogue.repository.query.TravelogueQueryRepository; +import kr.touroot.travelogue.repository.query.TravelogueQueryRepositoryImpl; import kr.touroot.travelplan.repository.PlaceTodoQueryRepository; import kr.touroot.travelplan.repository.PlaceTodoQueryRepositoryImpl; import lombok.RequiredArgsConstructor; diff --git a/backend/src/test/java/kr/touroot/tag/TagControllerTest.java b/backend/src/test/java/kr/touroot/tag/TagControllerTest.java index ba251054..53bfbfc0 100644 --- a/backend/src/test/java/kr/touroot/tag/TagControllerTest.java +++ b/backend/src/test/java/kr/touroot/tag/TagControllerTest.java @@ -41,7 +41,7 @@ void setUp() { @Test void createTag() { // given - TagCreateRequest request = TagFixture.TAG.getCreateRequest(); + TagCreateRequest request = TagFixture.TAG_1.getCreateRequest(); // when & then RestAssured.given().log().all() diff --git a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java index 19ec2a2e..dcd3e47d 100644 --- a/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java +++ b/backend/src/test/java/kr/touroot/tag/fixture/TagFixture.java @@ -6,7 +6,9 @@ public enum TagFixture { - TAG("강아지와 함께"), + TAG_1("강아지와 함께"), + TAG_2("고양이와 함께"), + TAG_3("알파카와 함께"), ; private final String tag; diff --git a/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java b/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java index 2d6a26e0..169ae524 100644 --- a/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java +++ b/backend/src/test/java/kr/touroot/tag/helper/TagTestHelper.java @@ -17,7 +17,7 @@ public TagTestHelper(TagRepository tagRepository) { } public Tag initTagData() { - Tag tag = TagFixture.TAG.get();; + Tag tag = TagFixture.TAG_1.get();; return tagRepository.save(tag); } diff --git a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java index 19ce1761..117b3fd4 100644 --- a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java +++ b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java @@ -40,7 +40,7 @@ void setUp() { @Test void validateDuplicated() { // given - Tag tag = TagFixture.TAG.get(); + Tag tag = TagFixture.TAG_1.get(); testHelper.initTagData(tag); TagCreateRequest request = new TagCreateRequest(tag.getTag()); diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 363bd7fe..17b0c92f 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -14,6 +14,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; +import kr.touroot.tag.fixture.TagFixture; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; @@ -249,6 +250,21 @@ void findMainPageTravelogues() throws JsonProcessingException { .body(is(objectMapper.writeValueAsString(responses))); } + @DisplayName("메인 페이지 조회 시, 최신 작성 순으로 여행기를 조회한다.") + @Test + void filterMainPageTravelogues() throws JsonProcessingException { + testHelper.initAllTravelogueTestData(); + testHelper.initTravelogueTestDataWithTag(member, List.of(TagFixture.TAG_2.get(), TagFixture.TAG_3.get())); + + RestAssured.given().log().all() + .accept(ContentType.JSON) + .params("tag-filter", "2,3") + .when().get("/api/v1/travelogues") + .then().log().all() + .statusCode(200).assertThat() + .body("content.size()", is(1)); + } + @DisplayName("존재하지 않는 여행기를 조회하면 예외가 발생한다.") @Test void findNotExistTravelogueThrowException() { diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index 2a4fbc60..a51664a7 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -59,7 +59,7 @@ public static TravelogueResponse getTravelogueResponseWithTag() { .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) - .tags(List.of(TagFixture.TAG.getResponse(1L))) + .tags(List.of(TagFixture.TAG_1.getResponse(1L))) .build(); } @@ -71,7 +71,7 @@ public static Page getTravelogueSimpleResponses() { .authorNickname("리비") .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") - .tags(List.of(TagFixture.TAG.getResponse(1L))) + .tags(List.of(TagFixture.TAG_1.getResponse(1L))) .build(), TravelogueSimpleResponse.builder() .id(1L) diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index 30b4ebb9..b619deae 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -107,14 +107,26 @@ public Travelogue initTravelogueTestDataWithTag(Member author) { Place position = persistPlace(); TraveloguePlace place = persistTraveloguePlace(position, day); persistTraveloguePhoto(place); - persisTravelogueTag(travelogue); + persisTravelogueTag(travelogue, TagFixture.TAG_1.get()); return travelogue; } - private void persisTravelogueTag(Travelogue travelogue) { - Tag tag = initTagTestData(); - travelogueTagRepository.save(new TravelogueTag(travelogue, tag)); + public Travelogue initTravelogueTestDataWithTag(Member author, List tags) { + Travelogue travelogue = persistTravelogue(author); + TravelogueDay day = persistTravelogueDay(travelogue); + Place position = persistPlace(); + TraveloguePlace place = persistTraveloguePlace(position, day); + persistTraveloguePhoto(place); + + tags.forEach(tag -> persisTravelogueTag(travelogue, tag)); + + return travelogue; + } + + private void persisTravelogueTag(Travelogue travelogue, Tag tag) { + Tag savedTag = initTagTestData(tag); + travelogueTagRepository.save(new TravelogueTag(travelogue, savedTag)); } public Member persistMember() { @@ -159,6 +171,10 @@ public Member initKakaoMemberTestData() { } public Tag initTagTestData() { - return tagRepository.save(TagFixture.TAG.get()); + return tagRepository.save(TagFixture.TAG_1.get()); + } + + public Tag initTagTestData(Tag tag) { + return tagRepository.save(tag); } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 91659a59..8dad6266 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -125,6 +125,21 @@ void findTravelogues() { assertThat(result).containsAll(expect); } + @DisplayName("필터링된 여행기 목록을 조회한다.") + @Test + void filterTravelogues() { + // given + testHelper.initAllTravelogueTestData(); + PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("id")); + List tagFilters = List.of(1L); + + // when + Page result = service.findSimpleTravelogues(tagFilters, pageRequest); + + // then + assertThat(result.getContent()).hasSize(1); + } + @DisplayName("제목 키워드를 기반으로 여행기 목록을 조회한다.") @Test void findTraveloguesByKeyword() { From f418f4ed47759de11561161d7430daa37423a002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=82=99=ED=97=8C?= <95845037+nak-honest@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:07:33 +0900 Subject: [PATCH 100/108] =?UTF-8?q?[Feature]=20-=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 여행기 좋아요 기능 구현 * style: swagger 메시지 수정 * feat: `@Transactional` 추가 * feat: 여행기와 좋아요를 누른 사용자에 대해 unique 제약 조건 추가 * fix: 컬럼명에 id 누락된 부분 추가 * feat: 여행기 좋아요 취소 기능 구현 * feat: 액세스 토큰이 존재하는 경우, 화이트 리스트의 요청도 `JwtAuthFilter`를 거치도록 변경 * feat: 여행기 상세 조회 시 좋아요 수, 좋아요 여부도 같이 응답하도록 변경 * feat: 메인 페이지에서 여행기 조회 시 좋아요 개수도 같이 응답하도록 변경 * refactor: JwtAuthFilter 메소드 분리 * docs: Swagger 응답 description 수정 * style: 클래스 첫 빈 줄 추가 * refactor: 좋아요 취소에 대한 단어를 전체적으로 `unlike`로 통일 * test: 401 예외 확인 테스트에서 메시지도 검증도 추가 * style: `.`이 하나만 존재할 때 줄바꿈 하지 않도록 컨벤션에 맞게 수정 --- .../kr/touroot/global/auth/JwtAuthFilter.java | 17 +++- .../controller/TravelogueController.java | 65 +++++++++++++ .../travelogue/domain/TravelogueLike.java | 40 ++++++++ .../dto/response/TravelogueLikeResponse.java | 11 +++ .../dto/response/TravelogueResponse.java | 14 ++- .../response/TravelogueSimpleResponse.java | 10 +- .../repository/TravelogueLikeRepository.java | 15 +++ .../service/TravelogueFacadeService.java | 44 ++++++++- .../service/TravelogueLikeService.java | 49 ++++++++++ .../controller/TravelogueControllerTest.java | 95 +++++++++++++++++- .../fixture/TravelogueResponseFixture.java | 24 +++++ .../helper/TravelogueTestHelper.java | 23 ++++- .../service/TravelogueFacadeServiceTest.java | 53 ++++++++++ .../service/TravelogueLikeServiceTest.java | 97 +++++++++++++++++++ 14 files changed, 542 insertions(+), 15 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/travelogue/domain/TravelogueLike.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueLikeResponse.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/TravelogueLikeRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java create mode 100644 backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java diff --git a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java index 239053d2..484b01ef 100644 --- a/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java +++ b/backend/src/main/java/kr/touroot/global/auth/JwtAuthFilter.java @@ -49,7 +49,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader(HttpHeaders.AUTHORIZATION); - if (token == null || token.isBlank()) { + if (isTokenBlank(token)) { sendUnauthorizedResponse(response, "로그인을 해주세요."); return; } @@ -79,12 +79,21 @@ private void sendUnauthorizedResponse(HttpServletResponse response, String messa @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - AntPathMatcher antPathMatcher = new AntPathMatcher(); - - String url = request.getRequestURI(); String method = request.getMethod(); + String requestURI = request.getRequestURI(); + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + + return isInWhiteList(method, requestURI) && isTokenBlank(token); + } + + private boolean isInWhiteList(String method, String url) { + AntPathMatcher antPathMatcher = new AntPathMatcher(); return WHITE_LIST.stream() .anyMatch(white -> white.method().matches(method) && antPathMatcher.match(white.urlPattern(), url)); } + + private boolean isTokenBlank(String token) { + return token == null || token.isBlank(); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java index eb53a18d..52976aeb 100644 --- a/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java +++ b/backend/src/main/java/kr/touroot/travelogue/controller/TravelogueController.java @@ -14,6 +14,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; +import kr.touroot.travelogue.dto.response.TravelogueLikeResponse; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.service.TravelogueFacadeService; @@ -23,6 +24,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -65,6 +67,29 @@ public ResponseEntity createTravelogue( .body(response); } + @Operation(summary = "여행기 좋아요") + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 여행기 ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 좋아요를 할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @PostMapping("/{id}/like") + public ResponseEntity likeTravelogue(@PathVariable Long id, @Valid MemberAuth member) { + return ResponseEntity.ok() + .body(travelogueFacadeService.likeTravelogue(id, member)); + } + @Operation(summary = "여행기 상세 조회") @ApiResponses(value = { @ApiResponse( @@ -82,6 +107,23 @@ public ResponseEntity findTravelogue(@PathVariable Long id) return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id)); } + @Operation(summary = "여행기 상세 조회") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 여행기 ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @GetMapping(value = "/{id}", headers = {HttpHeaders.AUTHORIZATION}) + public ResponseEntity findTravelogue(@PathVariable Long id, MemberAuth member) { + return ResponseEntity.ok(travelogueFacadeService.findTravelogueById(id, member)); + } + @Operation(summary = "여행기 메인 페이지 조회") @ApiResponses(value = { @ApiResponse( @@ -200,4 +242,27 @@ public ResponseEntity deleteTravelogue(@PathVariable Long id, MemberAuth m return ResponseEntity.noContent() .build(); } + + @Operation(summary = "여행기 좋아요 취소") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "존재하지 않는 여행기 ID로 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "401", + description = "로그인하지 않은 사용자가 좋아요를 취소 할 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + }) + @DeleteMapping("/{id}/like") + public ResponseEntity unlikeTravelogue(@PathVariable Long id, @Valid MemberAuth member) { + return ResponseEntity.ok() + .body(travelogueFacadeService.unlikeTravelogue(id, member)); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueLike.java b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueLike.java new file mode 100644 index 00000000..31acdd91 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/domain/TravelogueLike.java @@ -0,0 +1,40 @@ +package kr.touroot.travelogue.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import kr.touroot.member.domain.Member; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"TRAVELOGUE_ID", "LIKER_ID"})}) +@Entity +public class TravelogueLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "TRAVELOGUE_ID", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Travelogue travelogue; + + @JoinColumn(name = "LIKER_ID", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Member liker; + + public TravelogueLike(Travelogue travelogue, Member liker) { + this(null, travelogue, liker); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueLikeResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueLikeResponse.java new file mode 100644 index 00000000..78afb540 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueLikeResponse.java @@ -0,0 +1,11 @@ +package kr.touroot.travelogue.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record TravelogueLikeResponse( + @Schema(description = "사용자의 좋아요 여부", example = "true") + Boolean isLiked, + @Schema(description = "여행기의 좋아요 수", example = "10") + Long likeCount +) { +} diff --git a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java index 5f207912..d6ee6a34 100644 --- a/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java +++ b/backend/src/main/java/kr/touroot/travelogue/dto/response/TravelogueResponse.java @@ -26,10 +26,18 @@ public record TravelogueResponse( @Schema(description = "여행기 태그") List tags, @Schema(description = "여행기 일자 목록") - List days + List days, + @Schema(description = "여행기 좋아요 숫자", example = "10") + Long likeCount, + @Schema(description = "사용자의 여행기 좋아요 여부", example = "true") + Boolean isLiked ) { - public static TravelogueResponse of(Travelogue travelogue, List days, List tags) { + public static TravelogueResponse of( + Travelogue travelogue, + List days, + List tags, + TravelogueLikeResponse like) { return TravelogueResponse.builder() .id(travelogue.getId()) .createdAt(travelogue.getCreatedAt().toLocalDate()) @@ -40,6 +48,8 @@ public static TravelogueResponse of(Travelogue travelogue, List tags + List tags, + @Schema(description = "작성자 프로필 사진 URL", example = "10") + Long likeCount ) { - public static TravelogueSimpleResponse of(Travelogue travelogue, List tags) { + public static TravelogueSimpleResponse of( + Travelogue travelogue, + List tags, + TravelogueLikeResponse like) { return TravelogueSimpleResponse.builder() .id(travelogue.getId()) .title(travelogue.getTitle()) @@ -30,6 +35,7 @@ public static TravelogueSimpleResponse of(Travelogue travelogue, List { + + Long countByTravelogue(Travelogue travelogue); + + boolean existsByTravelogueAndLiker(Travelogue travelogue, Member liker); + + void deleteByTravelogueAndLiker(Travelogue travelogue, Member liker); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java index e1a61117..5c0bbe07 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueFacadeService.java @@ -17,6 +17,7 @@ import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; import kr.touroot.travelogue.dto.response.TravelogueDayResponse; +import kr.touroot.travelogue.dto.response.TravelogueLikeResponse; import kr.touroot.travelogue.dto.response.TraveloguePlaceResponse; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; @@ -35,6 +36,7 @@ public class TravelogueFacadeService { private final TraveloguePlaceService traveloguePlaceService; private final TraveloguePhotoService traveloguePhotoService; private final TravelogueTagService travelogueTagService; + private final TravelogueLikeService travelogueLikeService; private final MemberService memberService; @Transactional @@ -42,7 +44,8 @@ public TravelogueResponse createTravelogue(MemberAuth member, TravelogueRequest Member author = memberService.getById(member.memberId()); Travelogue travelogue = travelogueService.createTravelogue(author, request); List tags = travelogueTagService.createTravelogueTags(travelogue, request.tags()); - return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue), tags); + TravelogueLikeResponse like = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, author); + return TravelogueResponse.of(travelogue, createDays(request.days(), travelogue), tags, like); } private List createDays(List requests, Travelogue travelogue) { @@ -71,15 +74,38 @@ private List createPhotos(List requests, Travelo .toList(); } + @Transactional + public TravelogueLikeResponse likeTravelogue(Long travelogueId, MemberAuth member) { + Travelogue travelogue = travelogueService.getTravelogueById(travelogueId); + Member liker = memberService.getById(member.memberId()); + + return travelogueLikeService.likeTravelogue(travelogue, liker); + } + @Transactional(readOnly = true) public TravelogueResponse findTravelogueById(Long id) { Travelogue travelogue = travelogueService.getTravelogueById(id); return getTravelogueResponse(travelogue); } + @Transactional(readOnly = true) + public TravelogueResponse findTravelogueById(Long id, MemberAuth member) { + Travelogue travelogue = travelogueService.getTravelogueById(id); + return getTravelogueResponse(travelogue, member); + } + private TravelogueResponse getTravelogueResponse(Travelogue travelogue) { List tagResponses = travelogueTagService.readTagByTravelogue(travelogue); - return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses); + TravelogueLikeResponse likeResponse = travelogueLikeService.findLikeByTravelogue(travelogue); + return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses, likeResponse); + } + + private TravelogueResponse getTravelogueResponse(Travelogue travelogue, MemberAuth member) { + Member liker = memberService.getById(member.memberId()); + + List tagResponses = travelogueTagService.readTagByTravelogue(travelogue); + TravelogueLikeResponse likeResponse = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, liker); + return TravelogueResponse.of(travelogue, findDaysOfTravelogue(travelogue), tagResponses, likeResponse); } private List findDaysOfTravelogue(Travelogue travelogue) { @@ -125,7 +151,8 @@ public Page findSimpleTravelogues(Pageable pageable, T private TravelogueSimpleResponse getTravelogueSimpleResponse(Travelogue travelogue) { List tagResponses = travelogueTagService.readTagByTravelogue(travelogue); - return TravelogueSimpleResponse.of(travelogue, tagResponses); + TravelogueLikeResponse likeResponse = travelogueLikeService.findLikeByTravelogue(travelogue); + return TravelogueSimpleResponse.of(travelogue, tagResponses, likeResponse); } @Transactional @@ -135,10 +162,11 @@ public TravelogueResponse updateTravelogue(Long id, MemberAuth member, Travelogu Travelogue updatedTravelogue = travelogueService.update(id, author, request); List tags = travelogueTagService.updateTravelogueTags(travelogue, request.tags()); + TravelogueLikeResponse like = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, author); clearTravelogueContents(travelogue); - return TravelogueResponse.of(updatedTravelogue, createDays(request.days(), updatedTravelogue), tags); + return TravelogueResponse.of(updatedTravelogue, createDays(request.days(), updatedTravelogue), tags, like); } private void clearTravelogueContents(Travelogue travelogue) { @@ -156,4 +184,12 @@ public void deleteTravelogueById(Long id, MemberAuth member) { clearTravelogueContents(travelogue); travelogueService.delete(travelogue, author); } + + @Transactional + public TravelogueLikeResponse unlikeTravelogue(Long travelogueId, MemberAuth member) { + Travelogue travelogue = travelogueService.getTravelogueById(travelogueId); + Member liker = memberService.getById(member.memberId()); + + return travelogueLikeService.unlikeTravelogue(travelogue, liker); + } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java new file mode 100644 index 00000000..579435fb --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueLikeService.java @@ -0,0 +1,49 @@ +package kr.touroot.travelogue.service; + +import kr.touroot.member.domain.Member; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.domain.TravelogueLike; +import kr.touroot.travelogue.dto.response.TravelogueLikeResponse; +import kr.touroot.travelogue.repository.TravelogueLikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class TravelogueLikeService { + + private final TravelogueLikeRepository travelogueLikeRepository; + + @Transactional(readOnly = true) + public TravelogueLikeResponse findLikeByTravelogue(Travelogue travelogue) { + return new TravelogueLikeResponse(false, travelogueLikeRepository.countByTravelogue(travelogue)); + } + + @Transactional(readOnly = true) + public TravelogueLikeResponse findLikeByTravelogueAndLiker(Travelogue travelogue, Member liker) { + boolean exists = travelogueLikeRepository.existsByTravelogueAndLiker(travelogue, liker); + return new TravelogueLikeResponse(exists, travelogueLikeRepository.countByTravelogue(travelogue)); + } + + @Transactional + public TravelogueLikeResponse likeTravelogue(Travelogue travelogue, Member liker) { + boolean notExists = !travelogueLikeRepository.existsByTravelogueAndLiker(travelogue, liker); + if (notExists) { + TravelogueLike travelogueLike = new TravelogueLike(travelogue, liker); + travelogueLikeRepository.save(travelogueLike); + } + + return new TravelogueLikeResponse(true, travelogueLikeRepository.countByTravelogue(travelogue)); + } + + @Transactional + public TravelogueLikeResponse unlikeTravelogue(Travelogue travelogue, Member liker) { + boolean exists = travelogueLikeRepository.existsByTravelogueAndLiker(travelogue, liker); + if (exists) { + travelogueLikeRepository.deleteByTravelogueAndLiker(travelogue, liker); + } + + return new TravelogueLikeResponse(false, travelogueLikeRepository.countByTravelogue(travelogue)); + } +} diff --git a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java index 17b0c92f..7876fb7c 100644 --- a/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/controller/TravelogueControllerTest.java @@ -20,6 +20,7 @@ import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; +import kr.touroot.travelogue.dto.response.TravelogueLikeResponse; import kr.touroot.travelogue.dto.response.TravelogueResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; @@ -205,7 +206,47 @@ void createTravelogueWithNotLoginThrowException() { .body(request) .when().post("/api/v1/travelogues") .then().log().all() - .statusCode(401); + .statusCode(401) + .body("message", is("로그인을 해주세요.")); + } + + @DisplayName("여행기에 좋아요를 한다.") + @Test + void likeTravelogue() throws JsonProcessingException { + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(author); + TravelogueLikeResponse response = new TravelogueLikeResponse(true, 1L); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().post("/api/v1/travelogues/1/like") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + + @DisplayName("존재하지 않는 여행기에 좋아요를 하면 예외가 발생한다.") + @Test + void likeTravelogueWithNotExistThrowException() throws JsonProcessingException { + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().post("/api/v1/travelogues/1/like") + .then().log().all() + .statusCode(400).assertThat() + .body("message", is("존재하지 않는 여행기입니다.")); + } + + @DisplayName("여행기를 좋아요 할 때 로그인 되어 있지 않으면 예외가 발생한다.") + @Test + void likeTravelogueWithNotLoginThrowException() { + Member author = testHelper.initKakaoMemberTestData(); + testHelper.initTravelogueTestData(author); + + RestAssured.given().log().all() + .when().post("/api/v1/travelogues/1/like") + .then().log().all() + .statusCode(401) + .body("message", is("로그인을 해주세요.")); } @DisplayName("여행기를 상세 조회한다.") @@ -222,6 +263,21 @@ void findTravelogue() throws JsonProcessingException { .body(is(objectMapper.writeValueAsString(response))); } + @DisplayName("여행기에 좋아요를 누른 사용자가 여행기를 상세 조회한다.") + @Test + void findTravelogueWithLike() throws JsonProcessingException { + testHelper.initTravelogueTestDataWithLike(member); + TravelogueResponse response = TravelogueResponseFixture.getTravelogueResponseWithLike(); + + RestAssured.given().log().all() + .accept(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().get("/api/v1/travelogues/1") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + @DisplayName("태그가 있는 여행기를 상세 조회한다.") @Test void findTravelogueWithTags() throws JsonProcessingException { @@ -437,4 +493,41 @@ void deleteTravelogueWithNotAuthor() { .statusCode(403) .body("message", is("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다.")); } + + @DisplayName("여행기에 좋아요 취소를 한다.") + @Test + void unlikeTravelogue() throws JsonProcessingException { + testHelper.initTravelogueTestDataWithLike(member); + TravelogueLikeResponse response = new TravelogueLikeResponse(false, 0L); + + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travelogues/1/like") + .then().log().all() + .statusCode(200).assertThat() + .body(is(objectMapper.writeValueAsString(response))); + } + + @DisplayName("존재하지 않는 여행기에 좋아요 취소를 하면 예외가 발생한다.") + @Test + void unlikeTravelogueWithNotExistThrowException() throws JsonProcessingException { + RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .when().delete("/api/v1/travelogues/1/like") + .then().log().all() + .statusCode(400).assertThat() + .body("message", is("존재하지 않는 여행기입니다.")); + } + + @DisplayName("여행기 좋아요 취소를 할 때 로그인 되어 있지 않으면 예외가 발생한다.") + @Test + void unlikeTravelogueWithNotLoginThrowException() { + testHelper.initTravelogueTestDataWithLike(member); + + RestAssured.given().log().all() + .when().delete("/api/v1/travelogues/1/like") + .then().log().all() + .statusCode(401) + .body("message", is("로그인을 해주세요.")); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java index a51664a7..369e3503 100644 --- a/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java +++ b/backend/src/test/java/kr/touroot/travelogue/fixture/TravelogueResponseFixture.java @@ -32,6 +32,8 @@ public static TravelogueResponse getTravelogueResponse() { .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .tags(List.of()) + .isLiked(false) + .likeCount(0L) .build(); } @@ -46,6 +48,8 @@ public static TravelogueResponse getUpdatedTravelogueResponse() { .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail_2.png") .days(getUpdatedTravelogueDayResponses()) .tags(List.of()) + .isLiked(false) + .likeCount(0L) .build(); } @@ -60,6 +64,24 @@ public static TravelogueResponse getTravelogueResponseWithTag() { .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .days(getTravelogueDayResponses()) .tags(List.of(TagFixture.TAG_1.getResponse(1L))) + .likeCount(0L) + .isLiked(false) + .build(); + } + + public static TravelogueResponse getTravelogueResponseWithLike() { + return TravelogueResponse.builder() + .id(1L) + .title("제주에 하영 옵서") + .createdAt(LocalDate.now()) + .authorNickname("리비") + .authorId(2L) + .authorProfileImageUrl("https://dev.touroot.kr/temporary/profile.png") + .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") + .days(getTravelogueDayResponses()) + .tags(List.of()) + .isLiked(true) + .likeCount(1L) .build(); } @@ -72,6 +94,7 @@ public static Page getTravelogueSimpleResponses() { .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .tags(List.of(TagFixture.TAG_1.getResponse(1L))) + .likeCount(0L) .build(), TravelogueSimpleResponse.builder() .id(1L) @@ -80,6 +103,7 @@ public static Page getTravelogueSimpleResponses() { .authorProfileUrl("https://dev.touroot.kr/temporary/profile.png") .thumbnail("https://dev.touroot.kr/temporary/jeju_thumbnail.png") .tags(List.of()) + .likeCount(1L) .build() ); diff --git a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java index b619deae..b8b6c074 100644 --- a/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java +++ b/backend/src/test/java/kr/touroot/travelogue/helper/TravelogueTestHelper.java @@ -18,10 +18,12 @@ import kr.touroot.tag.repository.TagRepository; import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; +import kr.touroot.travelogue.domain.TravelogueLike; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.domain.TravelogueTag; import kr.touroot.travelogue.repository.TravelogueDayRepository; +import kr.touroot.travelogue.repository.TravelogueLikeRepository; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; import kr.touroot.travelogue.repository.TraveloguePlaceRepository; import kr.touroot.travelogue.repository.TravelogueRepository; @@ -40,6 +42,7 @@ public class TravelogueTestHelper { private final MemberRepository memberRepository; private final TagRepository tagRepository; private final TravelogueTagRepository travelogueTagRepository; + private final TravelogueLikeRepository travelogueLikeRepository; @Autowired public TravelogueTestHelper( @@ -50,7 +53,8 @@ public TravelogueTestHelper( TraveloguePhotoRepository traveloguePhotoRepository, MemberRepository memberRepository, TagRepository tagRepository, - TravelogueTagRepository travelogueTagRepository + TravelogueTagRepository travelogueTagRepository, + TravelogueLikeRepository travelogueLikeRepository ) { this.placeRepository = placeRepository; this.travelogueRepository = travelogueRepository; @@ -60,12 +64,14 @@ public TravelogueTestHelper( this.memberRepository = memberRepository; this.tagRepository = tagRepository; this.travelogueTagRepository = travelogueTagRepository; + this.travelogueLikeRepository = travelogueLikeRepository; } public void initAllTravelogueTestData() { Member author = persistMember(); - initTravelogueTestData(author); + Travelogue travelogue = initTravelogueTestData(author); initTravelogueTestDataWithTag(author); + persistTravelogueLike(travelogue, author); } public Travelogue initTravelogueTestData() { @@ -124,6 +130,13 @@ public Travelogue initTravelogueTestDataWithTag(Member author, List tags) { return travelogue; } + public Travelogue initTravelogueTestDataWithLike(Member liker) { + Travelogue travelogue = initTravelogueTestData(); + persistTravelogueLike(travelogue, liker); + + return travelogue; + } + private void persisTravelogueTag(Travelogue travelogue, Tag tag) { Tag savedTag = initTagTestData(tag); travelogueTagRepository.save(new TravelogueTag(travelogue, savedTag)); @@ -165,6 +178,12 @@ public TraveloguePhoto persistTraveloguePhoto(TraveloguePlace place) { return traveloguePhotoRepository.save(photo); } + public TravelogueLike persistTravelogueLike(Travelogue travelogue, Member liker) { + TravelogueLike like = new TravelogueLike(travelogue, liker); + + return travelogueLikeRepository.save(like); + } + public Member initKakaoMemberTestData() { Member member = new Member(1L, "리비", "https://dev.touroot.kr/temporary/profile.png", LoginType.KAKAO); return memberRepository.save(member); diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java index 8dad6266..37889a46 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueFacadeServiceTest.java @@ -14,11 +14,13 @@ import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.member.service.MemberService; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.dto.request.TravelogueRequest; import kr.touroot.travelogue.dto.request.TravelogueSearchRequest; +import kr.touroot.travelogue.dto.response.TravelogueLikeResponse; import kr.touroot.travelogue.dto.response.TravelogueSimpleResponse; import kr.touroot.travelogue.fixture.TravelogueRequestFixture; import kr.touroot.travelogue.fixture.TravelogueResponseFixture; @@ -42,6 +44,7 @@ TravelogueDayService.class, TraveloguePlaceService.class, TravelogueTagService.class, + TravelogueLikeService.class, MemberService.class, TravelogueTestHelper.class, AwsS3Provider.class, @@ -104,6 +107,26 @@ private List getTravelogueDayRequests() { return TravelogueRequestFixture.getTravelogueDayRequests(places); } + @DisplayName("여행기에 좋아요를 할 수 있다.") + @Test + void likeTravelogue() { + Travelogue travelogue = testHelper.initTravelogueTestData(); + Member liker = testHelper.initKakaoMemberTestData(); + + assertThat(service.likeTravelogue(travelogue.getId(), new MemberAuth(liker.getId()))) + .isEqualTo(new TravelogueLikeResponse(true, 1L)); + } + + @DisplayName("존재하지 않는 여행기에 좋아요를 하면 예외가 발생한다.") + @Test + void likeTravelogueWithNotExist() { + Member liker = testHelper.initKakaoMemberTestData(); + + assertThatThrownBy(() -> service.likeTravelogue(1L, new MemberAuth(liker.getId()))) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } + @DisplayName("여행기를 ID를 기준으로 조회한다.") @Test void findTravelogueById() { @@ -113,6 +136,16 @@ void findTravelogueById() { .isEqualTo(TravelogueResponseFixture.getTravelogueResponse()); } + @DisplayName("여행기를 ID와 로그인한 사용자를 기준으로 조회한다.") + @Test + void findTravelogueByIdAndLiker() { + Member liker = testHelper.initKakaoMemberTestData(); + Long travelogueId = testHelper.initTravelogueTestDataWithLike(liker).getId(); + + assertThat(service.findTravelogueById(travelogueId, new MemberAuth(liker.getId()))) + .isEqualTo(TravelogueResponseFixture.getTravelogueResponseWithLike()); + } + @DisplayName("메인 페이지에 표시할 여행기 목록을 조회한다.") @Test void findTravelogues() { @@ -240,4 +273,24 @@ void deleteByIdWithNotAuthor() { .isInstanceOf(ForbiddenException.class) .hasMessage("본인이 작성한 여행기만 수정하거나 삭제할 수 있습니다."); } + + @DisplayName("여행기에 좋아요를 취소 할 수 있다.") + @Test + void unlikeTravelogue() { + Member liker = testHelper.initKakaoMemberTestData(); + Travelogue travelogue = testHelper.initTravelogueTestDataWithLike(liker); + + assertThat(service.unlikeTravelogue(travelogue.getId(), new MemberAuth(liker.getId()))) + .isEqualTo(new TravelogueLikeResponse(false, 0L)); + } + + @DisplayName("존재하지 않는 여행기에 좋아요를 취소 하면 예외가 발생한다.") + @Test + void unlikeTravelogueWithNotExist() { + Member liker = testHelper.initKakaoMemberTestData(); + + assertThatThrownBy(() -> service.unlikeTravelogue(1L, new MemberAuth(liker.getId()))) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행기입니다."); + } } diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java new file mode 100644 index 00000000..6e6458ed --- /dev/null +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueLikeServiceTest.java @@ -0,0 +1,97 @@ +package kr.touroot.travelogue.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import kr.touroot.global.ServiceTest; +import kr.touroot.member.domain.Member; +import kr.touroot.travelogue.domain.Travelogue; +import kr.touroot.travelogue.dto.response.TravelogueLikeResponse; +import kr.touroot.travelogue.helper.TravelogueTestHelper; +import kr.touroot.utils.DatabaseCleaner; +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.context.annotation.Import; + +@DisplayName("여행기 좋아요 서비스") +@Import(value = {TravelogueLikeService.class, TravelogueTestHelper.class}) +@ServiceTest +class TravelogueLikeServiceTest { + + private final TravelogueLikeService travelogueLikeService; + private final DatabaseCleaner databaseCleaner; + private final TravelogueTestHelper testHelper; + + @Autowired + public TravelogueLikeServiceTest( + TravelogueLikeService travelogueLikeService, + DatabaseCleaner databaseCleaner, + TravelogueTestHelper testHelper + ) { + this.travelogueLikeService = travelogueLikeService; + this.databaseCleaner = databaseCleaner; + this.testHelper = testHelper; + } + + @BeforeEach + void setUp() { + databaseCleaner.executeTruncate(); + } + + @DisplayName("여행기를 기반으로 좋아요 정보를 조회할 수 있다.") + @Test + void findLikeByTravelogue() { + // given + Member liker = testHelper.initKakaoMemberTestData(); + Travelogue travelogue = testHelper.initTravelogueTestDataWithLike(liker); + + // when + TravelogueLikeResponse response = travelogueLikeService.findLikeByTravelogue(travelogue); + + // then + assertThat(response).isEqualTo(new TravelogueLikeResponse(false, 1L)); + } + + @DisplayName("여행기와 좋아요 한 사람을 기반으로 좋아요 정보를 조회할 수 있다.") + @Test + void findLikeByTravelogueAndLiker() { + // given + Member liker = testHelper.initKakaoMemberTestData(); + Travelogue travelogue = testHelper.initTravelogueTestDataWithLike(liker); + + // when + TravelogueLikeResponse response = travelogueLikeService.findLikeByTravelogueAndLiker(travelogue, liker); + + // then + assertThat(response).isEqualTo(new TravelogueLikeResponse(true, 1L)); + } + + @DisplayName("여행기에 좋아요를 할 수 있다.") + @Test + void likeTravelogue() { + // given + Travelogue travelogue = testHelper.initTravelogueTestData(); + Member liker = testHelper.initKakaoMemberTestData(); + + // when + TravelogueLikeResponse response = travelogueLikeService.likeTravelogue(travelogue, liker); + + // then + assertThat(response).isEqualTo(new TravelogueLikeResponse(true, 1L)); + } + + @DisplayName("여행기에 좋아요를 취소 할 수 있다.") + @Test + void unlikeTravelogue() { + // given + Member liker = testHelper.initKakaoMemberTestData(); + Travelogue travelogue = testHelper.initTravelogueTestDataWithLike(liker); + + // when + TravelogueLikeResponse response = travelogueLikeService.unlikeTravelogue(travelogue, liker); + + // then + assertThat(response).isEqualTo(new TravelogueLikeResponse(false, 0L)); + } +} From 177091b82b07b3f47922e672b8d0f3a9fdf5a285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 19 Aug 2024 04:41:27 +0900 Subject: [PATCH 101/108] =?UTF-8?q?[Feature]=20-=20Local=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=EC=84=9C=EB=8A=94=20=EC=9E=84=EB=B2=A0?= =?UTF-8?q?=EB=94=94=EB=93=9C=20S3=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=A0=81=EC=9A=A9=20(#322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: local, test 환경에서 작동하는 Embedded S3 Client 구현 - S3Client 빈 프로필 분리 - EmbeddedS3는 랜덤한 동적 포트 번호 사용 * feat: 이미지 Post 요청이 MultipartFile만 받도록 설정 --- backend/build.gradle | 2 + .../kr/touroot/global/config/AwsS3Config.java | 23 +++++++ .../global/config/EmbeddedS3Config.java | 63 +++++++++++++++++++ .../image/controller/ImageController.java | 3 +- .../image/infrastructure/AwsS3Provider.java | 36 +++++------ .../infrastructure/AwsS3ProviderTest.java | 61 +++++++++--------- 6 files changed, 134 insertions(+), 54 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/global/config/AwsS3Config.java create mode 100644 backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java diff --git a/backend/build.gradle b/backend/build.gradle index 2ab14a0a..5a45039d 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation 'software.amazon.awssdk:s3:2.20.28' implementation 'software.amazon.awssdk:sts:2.20.28' implementation 'software.amazon.awssdk:auth:2.20.28' + implementation 'io.findify:s3mock_2.13:0.2.6' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' @@ -44,6 +45,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.5.0' + testImplementation 'io.findify:s3mock_2.13:0.2.6' testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/backend/src/main/java/kr/touroot/global/config/AwsS3Config.java b/backend/src/main/java/kr/touroot/global/config/AwsS3Config.java new file mode 100644 index 00000000..1d411dd5 --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/config/AwsS3Config.java @@ -0,0 +1,23 @@ +package kr.touroot.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +@Profile({"dev"}) +public class AwsS3Config { + + @Primary + @Bean(name = "s3Client", destroyMethod = "close") + public S3Client s3Client() { + return S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(InstanceProfileCredentialsProvider.create()) + .build(); + } +} diff --git a/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java b/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java new file mode 100644 index 00000000..d9632215 --- /dev/null +++ b/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java @@ -0,0 +1,63 @@ +package kr.touroot.global.config; + +import io.findify.s3mock.S3Mock; +import java.net.URI; +import java.util.Random; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; + +@Configuration +public class EmbeddedS3Config { + + private static final int DYNAMIC_PORT_NUMBER_LOWER = 49152; + private static final int DYNAMIC_PORT_NUMBER_RANGE = 16384; + + private final String bucketName; + private final int port; + + public EmbeddedS3Config(@Value("${cloud.aws.s3.bucket}") String bucketName) { + this.bucketName = bucketName; + this.port = getRandomPortNumber(); + } + + private int getRandomPortNumber() { + Random random = new Random(); + return random.nextInt(DYNAMIC_PORT_NUMBER_RANGE) + DYNAMIC_PORT_NUMBER_LOWER; + } + + @Bean(name = "s3Mock", initMethod = "start", destroyMethod = "shutdown") + public S3Mock s3Mock() { + return new S3Mock.Builder() + .withPort(port) + .withInMemoryBackend() + .build(); + } + + @DependsOn({"s3Mock"}) + @Bean(name = "s3Client", destroyMethod = "close") + public S3Client s3Client() { + S3Client s3Client = S3Client.builder() + .forcePathStyle(true) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .region(Region.AP_NORTHEAST_2) + .endpointOverride(URI.create("http://localhost:" + port)) + .build(); + + createLocalBucket(s3Client); + return s3Client; + } + + private void createLocalBucket(S3Client s3Client) { + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder() + .bucket(bucketName) + .build(); + + s3Client.createBucket(createBucketRequest); + } +} diff --git a/backend/src/main/java/kr/touroot/image/controller/ImageController.java b/backend/src/main/java/kr/touroot/image/controller/ImageController.java index 1348ae8f..493b0800 100644 --- a/backend/src/main/java/kr/touroot/image/controller/ImageController.java +++ b/backend/src/main/java/kr/touroot/image/controller/ImageController.java @@ -10,6 +10,7 @@ import kr.touroot.global.exception.dto.ExceptionResponse; import kr.touroot.image.service.ImageService; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,7 +38,7 @@ public class ImageController { content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) ) }) - @PostMapping + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> uploadImages(@RequestPart List files) { List imageUrls = imageService.uploadImages(files); return ResponseEntity.ok(imageUrls); diff --git a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java index 7a785d29..596bc32e 100644 --- a/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java +++ b/backend/src/main/java/kr/touroot/image/infrastructure/AwsS3Provider.java @@ -9,9 +9,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.CopyObjectRequest; import software.amazon.awssdk.services.s3.model.NoSuchKeyException; @@ -20,6 +18,7 @@ @Component public class AwsS3Provider { + private final S3Client s3Client; private final String bucket; private final String imageBaseUri; private final String tourootStoragePath; @@ -27,12 +26,14 @@ public class AwsS3Provider { private final String imageStoragePath; public AwsS3Provider( + S3Client s3Client, @Value("${cloud.aws.s3.bucket}") String bucket, @Value("${cloud.aws.s3.image-base-uri}") String imageBaseUri, @Value("${cloud.aws.s3.base-storage-path}") String tourootStoragePath, @Value("${cloud.aws.s3.temporary-storage-path}") String temporaryStoragePath, @Value("${cloud.aws.s3.image-storage-path}") String imageStoragePath ) { + this.s3Client = s3Client; this.bucket = bucket; this.imageBaseUri = imageBaseUri; this.tourootStoragePath = tourootStoragePath; @@ -43,31 +44,22 @@ public AwsS3Provider( public List uploadImages(List files) { List urls = new ArrayList<>(); - try (S3Client s3Client = getS3Client()) { - files.stream() - .map(ImageFile::getFile) - .forEach(file -> { - String newFileName = createNewFileName(file.getOriginalFilename()); - String filePath = tourootStoragePath + temporaryStoragePath + newFileName; - uploadFile(file, filePath, s3Client); - String s3Key = imageBaseUri + temporaryStoragePath + newFileName; - urls.add(s3Key); - }); - return urls; - } + files.stream() + .map(ImageFile::getFile) + .forEach(file -> { + String newFileName = createNewFileName(file.getOriginalFilename()); + String filePath = tourootStoragePath + temporaryStoragePath + newFileName; + uploadFile(file, filePath, s3Client); + String s3Key = imageBaseUri + temporaryStoragePath + newFileName; + urls.add(s3Key); + }); + return urls; } private String createNewFileName(String fileName) { return UUID.randomUUID() + fileName.substring(fileName.lastIndexOf(".")); } - S3Client getS3Client() { - return S3Client.builder() - .region(Region.AP_NORTHEAST_2) - .credentialsProvider(InstanceProfileCredentialsProvider.create()) - .build(); - } - private void uploadFile(MultipartFile file, String filePath, S3Client s3Client) { try { RequestBody requestBody = RequestBody.fromBytes(file.getBytes()); @@ -100,7 +92,7 @@ private void validateS3Path(String imageKey) { } private void copyFile(String sourceKey, String destinationKey) { - try (S3Client s3Client = getS3Client()) { + try { CopyObjectRequest request = CopyObjectRequest.builder() .sourceBucket(bucket) .sourceKey(sourceKey) diff --git a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java index 3d8ce5b1..64bc3d43 100644 --- a/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java +++ b/backend/src/test/java/kr/touroot/image/infrastructure/AwsS3ProviderTest.java @@ -2,49 +2,48 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; +import java.util.List; +import kr.touroot.global.AcceptanceTest; import kr.touroot.global.exception.S3UploadException; +import kr.touroot.image.domain.ImageFile; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.model.CopyObjectRequest; -import software.amazon.awssdk.services.s3.model.CopyObjectResponse; - -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = AwsS3Provider.class) -class AwsS3ProviderTest { - - @MockBean - private S3Client s3Client; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; - @SpyBean - private AwsS3Provider s3Provider; +@AcceptanceTest +class AwsS3ProviderTest { - @Value("${cloud.aws.s3.image-base-uri}") - private String imageBaseUri; - @Value("${cloud.aws.s3.temporary-storage-path}") - private String temporaryStoragePath; - @Value("${cloud.aws.s3.image-storage-path}") - private String imageStoragePath; + private final AwsS3Provider s3Provider; + private final String temporaryStoragePath; + private final String imageStoragePath; + + public AwsS3ProviderTest( + @Autowired AwsS3Provider s3Provider, + @Value("${cloud.aws.s3.temporary-storage-path}") String temporaryStoragePath, + @Value("${cloud.aws.s3.image-storage-path}") String imageStoragePath + ) { + this.s3Provider = s3Provider; + this.temporaryStoragePath = temporaryStoragePath; + this.imageStoragePath = imageStoragePath; + } @DisplayName("유효한 url을 통해 이미지를 영구 폴더로 복사하면 새로운 url을 반환한다.") @Test void copyImageToPermanentStorage() { - when(s3Provider.getS3Client()) - .thenReturn(s3Client); - when(s3Client.copyObject(any(CopyObjectRequest.class))) - .thenReturn(CopyObjectResponse.builder().build()); + MultipartFile multipartFile = new MockMultipartFile( + "file", + "test-image.png", + "image/png", + "test image content".getBytes() + ); + String temporaryUrl = s3Provider.uploadImages(List.of(new ImageFile(multipartFile))) + .get(0); + String imageUrl = temporaryUrl.replace(temporaryStoragePath, imageStoragePath); - String temporaryUrl = imageBaseUri + temporaryStoragePath + "valid.png"; - String imageUrl = imageBaseUri + imageStoragePath + "valid.png"; assertThat(s3Provider.copyImageToPermanentStorage(temporaryUrl)) .isEqualTo(imageUrl); } From 385f5b85a8b1a349b820c461f7ac96194bb1aab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:23:01 +0900 Subject: [PATCH 102/108] =?UTF-8?q?[Fix]=20bean=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: 빈이 충돌하지 않도록 EmbeddedS3Config 프로필 지정 --- .../main/java/kr/touroot/global/config/EmbeddedS3Config.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java b/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java index d9632215..8d38c133 100644 --- a/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java +++ b/backend/src/main/java/kr/touroot/global/config/EmbeddedS3Config.java @@ -7,12 +7,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Profile; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.CreateBucketRequest; @Configuration +@Profile({"default", "local"}) public class EmbeddedS3Config { private static final int DYNAMIC_PORT_NUMBER_LOWER = 49152; From a59cbf61ad3974326ed0a3bea4c1a4abdf6f9b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=ED=9C=98=EC=9A=A9?= <99064014+slimsha2dy@users.noreply.github.com> Date: Mon, 19 Aug 2024 22:35:59 +0900 Subject: [PATCH 103/108] =?UTF-8?q?[Feature]=20-=20=EC=97=AC=ED=96=89=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#341)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Todo 요청 객체가 체크 여부를 받도록 수정 * feat: 여행 계획 수정 서비스 구현 * feat: 여행 계획 수정 컨트롤러 구현 * style: 요청 객체 네이밍에서 create 제거 * style: 메서드명 수정 * fix: 잘못 입력된 문자 삭제 --- .../controller/TravelPlanController.java | 31 ++++- .../touroot/travelplan/domain/TravelPlan.java | 5 + ...CreateRequest.java => PlanDayRequest.java} | 4 +- ...eateRequest.java => PlanPlaceRequest.java} | 4 +- .../dto/request/PlanPlaceTodoRequest.java | 10 +- ...eRequest.java => PlanPositionRequest.java} | 2 +- ...lanCreateRequest.java => PlanRequest.java} | 4 +- .../travelplan/service/TravelPlanService.java | 53 ++++++-- .../controller/TravelPlanControllerTest.java | 119 +++++++++++++++--- .../service/TravelPlanServiceTest.java | 112 ++++++++++++++--- 10 files changed, 287 insertions(+), 57 deletions(-) rename backend/src/main/java/kr/touroot/travelplan/dto/request/{PlanDayCreateRequest.java => PlanDayRequest.java} (89%) rename backend/src/main/java/kr/touroot/travelplan/dto/request/{PlanPlaceCreateRequest.java => PlanPlaceRequest.java} (92%) rename backend/src/main/java/kr/touroot/travelplan/dto/request/{PlanPositionCreateRequest.java => PlanPositionRequest.java} (92%) rename backend/src/main/java/kr/touroot/travelplan/dto/request/{PlanCreateRequest.java => PlanRequest.java} (94%) diff --git a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java index d8ee6202..8d9bc59a 100644 --- a/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java +++ b/backend/src/main/java/kr/touroot/travelplan/controller/TravelPlanController.java @@ -12,7 +12,7 @@ import java.util.UUID; import kr.touroot.global.auth.dto.MemberAuth; import kr.touroot.global.exception.dto.ExceptionResponse; -import kr.touroot.travelplan.dto.request.PlanCreateRequest; +import kr.touroot.travelplan.dto.request.PlanRequest; import kr.touroot.travelplan.dto.response.PlanCreateResponse; import kr.touroot.travelplan.dto.response.PlanResponse; import kr.touroot.travelplan.service.TravelPlanService; @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -53,7 +54,7 @@ public class TravelPlanController { }) @PostMapping public ResponseEntity createTravelPlan( - @Valid @RequestBody PlanCreateRequest request, + @Valid @RequestBody PlanRequest request, MemberAuth memberAuth ) { PlanCreateResponse data = travelPlanService.createTravelPlan(request, memberAuth); @@ -87,6 +88,32 @@ public ResponseEntity readTravelPlan( return ResponseEntity.ok(data); } + @Operation(summary = "여행 계획 수정") + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "요청이 정상적으로 처리되었을 때" + ), + @ApiResponse( + responseCode = "400", + description = "요청 Body에 올바르지 않은 값이 전달되었을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ), + @ApiResponse( + responseCode = "403", + description = "작성자가 아닌 사용자가 요청했을 때", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) + @PutMapping("/{id}") + public ResponseEntity updateTravelPlan( + @PathVariable Long id, + @Valid MemberAuth memberAuth, + @Valid @RequestBody PlanRequest request + ) { + return ResponseEntity.ok(travelPlanService.updateTravelPlan(id, memberAuth, request)); + } + @Operation(summary = "여행 계획 삭제") @ApiResponses(value = { @ApiResponse( diff --git a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java index 42193f65..85d7e050 100644 --- a/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java +++ b/backend/src/main/java/kr/touroot/travelplan/domain/TravelPlan.java @@ -83,6 +83,11 @@ private void validateTitleLength(String title) { } } + public void update(String title, LocalDate startDate) { + this.title = title; + this.startDate = startDate; + } + public boolean isStartDateBefore(LocalDate date) { return startDate.isBefore(date); } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayRequest.java similarity index 89% rename from backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayRequest.java index 4fbcc91b..f2c83802 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanDayRequest.java @@ -8,12 +8,12 @@ import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.domain.TravelPlanDay; -public record PlanDayCreateRequest( +public record PlanDayRequest( @Schema(description = "여행 장소 정보") @Valid @Size(min = 1, message = "여행 장소는 한 개 이상이어야 합니다.") @NotNull(message = "여행 장소 정보는 비어있을 수 없습니다.") - List places + List places ) { public TravelPlanDay toPlanDay(int order, TravelPlan plan) { diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java similarity index 92% rename from backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java index d6ea332d..ff12a31f 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceRequest.java @@ -11,12 +11,12 @@ import lombok.Builder; @Builder -public record PlanPlaceCreateRequest( +public record PlanPlaceRequest( @Schema(description = "여행 장소 이름", example = "잠실한강공원") @NotBlank(message = "장소명은 비어있을 수 없습니다.") String placeName, @Valid @NotNull(message = "위치는 비어있을 수 없습니다.") - PlanPositionCreateRequest position, + PlanPositionRequest position, @Valid @NotNull(message = "TODO 리스트는 필수 입니다.") List todos diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java index a9b9131e..7664988d 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPlaceTodoRequest.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import kr.touroot.travelplan.domain.TravelPlaceTodo; import kr.touroot.travelplan.domain.TravelPlanPlace; @@ -10,10 +11,13 @@ public record PlanPlaceTodoRequest( @Schema(description = "여행 장소에서 진행할 TODO", example = "함덕 해수욕장 산책") @NotBlank(message = "TODO 내용은 비어 있을 수 없습니다") @Size(min = 1, max = 20, message = "TODO 내용은 1자에서 20자 사이의 길이를 가져야 합니다") - String content + String content, + @Schema(description = "TODO의 체크 여부", example = "true") + @NotNull(message = "TODO의 체크 여부는 비어 있을 수 없습니다.") + Boolean isChecked ) { - public TravelPlaceTodo toUncheckedPlaceTodo(TravelPlanPlace travelPlanPlace, Integer order) { - return new TravelPlaceTodo(travelPlanPlace, content, order, false); + public TravelPlaceTodo toPlaceTodo(TravelPlanPlace travelPlanPlace, Integer order) { + return new TravelPlaceTodo(travelPlanPlace, content, order, isChecked); } } diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionRequest.java similarity index 92% rename from backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionRequest.java index f6ccaf1e..3303b8b8 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanPositionRequest.java @@ -3,7 +3,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -public record PlanPositionCreateRequest( +public record PlanPositionRequest( @Schema(description = "여행 장소 위도", example = "37.5175896") @NotNull(message = "위도는 비어있을 수 없습니다.") String lat, diff --git a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanCreateRequest.java b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanRequest.java similarity index 94% rename from backend/src/main/java/kr/touroot/travelplan/dto/request/PlanCreateRequest.java rename to backend/src/main/java/kr/touroot/travelplan/dto/request/PlanRequest.java index fff10f21..9cd1d56e 100644 --- a/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanCreateRequest.java +++ b/backend/src/main/java/kr/touroot/travelplan/dto/request/PlanRequest.java @@ -13,7 +13,7 @@ import lombok.Builder; @Builder -public record PlanCreateRequest( +public record PlanRequest( @Schema(description = "여행 계획 제목", example = "신나는 잠실 한강 여행") @NotBlank(message = "여행 계획 제목은 비어있을 수 없습니다.") String title, @@ -24,7 +24,7 @@ public record PlanCreateRequest( @Valid @Size(min = 1, message = "여행 날짜는 하루 이상 있어야 합니다.") @NotNull(message = "여행 날짜 정보는 비어있을 수 없습니다.") - List days + List days ) { public TravelPlan toTravelPlan(Member author, UUID shareKey) { diff --git a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java index f1643866..99e139ad 100644 --- a/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java +++ b/backend/src/main/java/kr/touroot/travelplan/service/TravelPlanService.java @@ -15,10 +15,10 @@ import kr.touroot.travelplan.domain.TravelPlan; import kr.touroot.travelplan.domain.TravelPlanDay; import kr.touroot.travelplan.domain.TravelPlanPlace; -import kr.touroot.travelplan.dto.request.PlanCreateRequest; -import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; -import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; +import kr.touroot.travelplan.dto.request.PlanDayRequest; +import kr.touroot.travelplan.dto.request.PlanPlaceRequest; import kr.touroot.travelplan.dto.request.PlanPlaceTodoRequest; +import kr.touroot.travelplan.dto.request.PlanRequest; import kr.touroot.travelplan.dto.response.PlanCreateResponse; import kr.touroot.travelplan.dto.response.PlanDayResponse; import kr.touroot.travelplan.dto.response.PlanPlaceResponse; @@ -46,10 +46,10 @@ public class TravelPlanService { private final PlaceTodoRepository placeTodoRepository; @Transactional - public PlanCreateResponse createTravelPlan(PlanCreateRequest request, MemberAuth memberAuth) { + public PlanCreateResponse createTravelPlan(PlanRequest request, MemberAuth memberAuth) { Member author = getMemberByMemberAuth(memberAuth); TravelPlan travelPlan = request.toTravelPlan(author, UUID.randomUUID()); - validateTravelPlan(travelPlan); + validateCreateTravelPlan(travelPlan); TravelPlan savedTravelPlan = travelPlanRepository.save(travelPlan); createPlanDay(request.days(), savedTravelPlan); @@ -57,7 +57,7 @@ public PlanCreateResponse createTravelPlan(PlanCreateRequest request, MemberAuth return new PlanCreateResponse(savedTravelPlan.getId()); } - private void validateTravelPlan(TravelPlan travelPlan) { + private void validateCreateTravelPlan(TravelPlan travelPlan) { if (travelPlan.isStartDateBefore(LocalDate.now())) { throw new BadRequestException("지난 날짜에 대한 계획은 작성할 수 없습니다."); } @@ -68,17 +68,17 @@ private Member getMemberByMemberAuth(MemberAuth memberAuth) { .orElseThrow(() -> new BadRequestException("존재하지 않는 사용자입니다.")); } - private void createPlanDay(List request, TravelPlan savedTravelPlan) { + private void createPlanDay(List request, TravelPlan savedTravelPlan) { for (int order = 0; order < request.size(); order++) { - PlanDayCreateRequest dayRequest = request.get(order); + PlanDayRequest dayRequest = request.get(order); TravelPlanDay travelPlanDay = travelPlanDayRepository.save(dayRequest.toPlanDay(order, savedTravelPlan)); createPlanPlace(dayRequest.places(), travelPlanDay); } } - private void createPlanPlace(List request, TravelPlanDay travelPlanDay) { + private void createPlanPlace(List request, TravelPlanDay travelPlanDay) { for (int order = 0; order < request.size(); order++) { - PlanPlaceCreateRequest planRequest = request.get(order); + PlanPlaceRequest planRequest = request.get(order); Place place = getPlace(planRequest); TravelPlanPlace planPlace = planRequest.toPlanPlace(order, travelPlanDay, place); TravelPlanPlace travelPlanPlace = travelPlanPlaceRepository.save(planPlace); @@ -89,12 +89,12 @@ private void createPlanPlace(List request, TravelPlanDay private void createPlaceTodo(List request, TravelPlanPlace travelPlanPlace) { for (int order = 0; order < request.size(); order++) { PlanPlaceTodoRequest todoRequest = request.get(order); - TravelPlaceTodo travelPlaceTodo = todoRequest.toUncheckedPlaceTodo(travelPlanPlace, order); + TravelPlaceTodo travelPlaceTodo = todoRequest.toPlaceTodo(travelPlanPlace, order); placeTodoRepository.save(travelPlaceTodo); } } - private Place getPlace(PlanPlaceCreateRequest planRequest) { + private Place getPlace(PlanPlaceRequest planRequest) { return placeRepository.findByNameAndLatitudeAndLongitude( planRequest.placeName(), planRequest.position().lat(), @@ -173,6 +173,35 @@ public int calculateTravelPeriod(TravelPlan travelPlan) { .size(); } + @Transactional + public PlanCreateResponse updateTravelPlan(Long planId, MemberAuth memberAuth, PlanRequest request) { + TravelPlan travelPlan = getTravelPlanById(planId); + Member author = getMemberByMemberAuth(memberAuth); + validateUpdateByAuthor(travelPlan, author); + + clearTravelPlanContents(travelPlan); + updateTravelPlanContents(request, travelPlan); + return new PlanCreateResponse(travelPlan.getId()); + } + + private void validateUpdateByAuthor(TravelPlan travelPlan, Member member) { + if (!travelPlan.isAuthor(member)) { + throw new ForbiddenException("여행 계획 수정은 작성자만 가능합니다."); + } + } + + private void clearTravelPlanContents(TravelPlan travelPlan) { + placeTodoRepository.deleteByTravelPlanPlaceDayPlan(travelPlan); + travelPlanPlaceRepository.deleteByDayPlan(travelPlan); + travelPlanDayRepository.deleteByPlan(travelPlan); + } + + private void updateTravelPlanContents(PlanRequest request, TravelPlan travelPlan) { + travelPlan.update(request.title(), request.startDate()); + travelPlanRepository.save(travelPlan); + createPlanDay(request.days(), travelPlan); + } + @Transactional public void deleteByTravelPlanId(Long planId, MemberAuth memberAuth) { TravelPlan travelPlan = getTravelPlanById(planId); diff --git a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java index a89b55c5..ff2818ce 100644 --- a/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/controller/TravelPlanControllerTest.java @@ -13,10 +13,10 @@ import kr.touroot.global.AcceptanceTest; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; -import kr.touroot.travelplan.dto.request.PlanCreateRequest; -import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; -import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; -import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; +import kr.touroot.travelplan.dto.request.PlanDayRequest; +import kr.touroot.travelplan.dto.request.PlanPlaceRequest; +import kr.touroot.travelplan.dto.request.PlanPositionRequest; +import kr.touroot.travelplan.dto.request.PlanRequest; import kr.touroot.travelplan.helper.TravelPlanTestHelper; import kr.touroot.utils.DatabaseCleaner; import org.junit.jupiter.api.BeforeEach; @@ -65,18 +65,18 @@ void setUp() { @Test void createTravelPlan() { // given - PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); - PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - PlanCreateRequest request = PlanCreateRequest.builder() + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MAX) - .days(List.of(planDayCreateRequest)) + .days(List.of(planDayRequest)) .build(); // when & then @@ -95,17 +95,17 @@ void createTravelPlan() { @Test void createTravelPlanWithInvalidStartDate() { // given - PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); - PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - PlanCreateRequest request = PlanCreateRequest.builder() + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MIN) - .days(List.of(planDayCreateRequest)) + .days(List.of(planDayRequest)) .build(); // when & then @@ -246,6 +246,97 @@ void readTravelPlanByInvalidShareKey() { .body("message", is("존재하지 않는 여행 계획입니다.")); } + @DisplayName("여행기를 수정한다.") + @Test + void updateTravelPlan() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(member); + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() + .placeName("잠실한강공원") + .todos(Collections.EMPTY_LIST) + .position(locationRequest) + .build(); + + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().put("/api/v1/travel-plans/" + travelPlan.getId()) + .then().log().all() + .statusCode(200) + .body("id", is(1)); + } + + @DisplayName("존재하지 않는 여행 계획 수정시 400를 응답한다.") + @Test + void updateTravelPlanWithNonExist() { + // given + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() + .placeName("잠실한강공원") + .todos(Collections.EMPTY_LIST) + .position(locationRequest) + .build(); + + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .body(request) + .when().put("/api/v1/travel-plans/" + 1) + .then().log().all() + .statusCode(400) + .body("message", is("존재하지 않는 여행 계획입니다.")); + } + + @DisplayName("작성자가 아닌 사용자가 여행 계획 수정시 403을 응답한다.") + @Test + void updateTravelPlanWithNotAuthor() { + // given + long id = testHelper.initTravelPlanTestData(member).getId(); + Member notAuthor = testHelper.initMemberTestData(); + String notAuthorAccessToken = jwtTokenProvider.createToken(notAuthor.getId()).accessToken(); + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() + .placeName("잠실한강공원") + .todos(Collections.EMPTY_LIST) + .position(locationRequest) + .build(); + + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayRequest)) + .build(); + + // when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + notAuthorAccessToken) + .body(request) + .when().put("/api/v1/travel-plans/" + id) + .then().log().all() + .statusCode(403) + .body("message", is("여행 계획 수정은 작성자만 가능합니다.")); + } + @DisplayName("여행계획을 삭제한다.") @Test void deleteTravelPlan() { diff --git a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java index d71769f3..d591b6bd 100644 --- a/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelplan/service/TravelPlanServiceTest.java @@ -14,10 +14,10 @@ import kr.touroot.global.exception.ForbiddenException; import kr.touroot.member.domain.Member; import kr.touroot.travelplan.domain.TravelPlan; -import kr.touroot.travelplan.dto.request.PlanCreateRequest; -import kr.touroot.travelplan.dto.request.PlanDayCreateRequest; -import kr.touroot.travelplan.dto.request.PlanPlaceCreateRequest; -import kr.touroot.travelplan.dto.request.PlanPositionCreateRequest; +import kr.touroot.travelplan.dto.request.PlanDayRequest; +import kr.touroot.travelplan.dto.request.PlanPlaceRequest; +import kr.touroot.travelplan.dto.request.PlanPositionRequest; +import kr.touroot.travelplan.dto.request.PlanRequest; import kr.touroot.travelplan.dto.response.PlanCreateResponse; import kr.touroot.travelplan.dto.response.PlanResponse; import kr.touroot.travelplan.helper.TravelPlanTestHelper; @@ -67,17 +67,17 @@ void setUp() { @Test void createTravelPlan() { // given - PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); - PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - PlanCreateRequest request = PlanCreateRequest.builder() + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MAX) - .days(List.of(planDayCreateRequest)) + .days(List.of(planDayRequest)) .build(); // when @@ -91,17 +91,17 @@ void createTravelPlan() { @Test void createTravelPlanWithInvalidStartDate() { // given - PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); - PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() .placeName("잠실한강공원") .position(locationRequest) .todos(Collections.EMPTY_LIST) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - PlanCreateRequest request = PlanCreateRequest.builder() + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.MIN) - .days(List.of(planDayCreateRequest)) + .days(List.of(planDayRequest)) .build(); // when & then= @@ -114,17 +114,17 @@ void createTravelPlanWithInvalidStartDate() { @Test void createTravelPlanStartsAtToday() { // given - PlanPositionCreateRequest locationRequest = new PlanPositionCreateRequest("37.5175896", "127.0867236"); - PlanPlaceCreateRequest planPlaceCreateRequest = PlanPlaceCreateRequest.builder() + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() .placeName("잠실한강공원") .todos(Collections.EMPTY_LIST) .position(locationRequest) .build(); - PlanDayCreateRequest planDayCreateRequest = new PlanDayCreateRequest(List.of(planPlaceCreateRequest)); - PlanCreateRequest request = PlanCreateRequest.builder() + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() .title("신나는 한강 여행") .startDate(LocalDate.now()) - .days(List.of(planDayCreateRequest)) + .days(List.of(planDayRequest)) .build(); // when & then= @@ -184,6 +184,80 @@ void calculateTravelPeriod() { assertThat(actual).isEqualTo(1); } + @DisplayName("여행 계획 서비스는 새로운 정보로 여행 계획을 수정한다.") + @Test + void updateTravelPlan() { + // given + TravelPlan travelPlan = testHelper.initTravelPlanTestData(author); + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() + .placeName("잠실한강공원") + .todos(Collections.EMPTY_LIST) + .position(locationRequest) + .build(); + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayRequest)) + .build(); + + // when + PlanCreateResponse updatedTravelPlan = travelPlanService.updateTravelPlan(travelPlan.getId(), memberAuth, + request); + + // then + assertThat(updatedTravelPlan.id()).isEqualTo(1L); + } + + @DisplayName("여행 계획 서비스는 존재하지 않는 여행 계획 수정 시 예외를 반환한다.") + @Test + void updateTravelPlanWitNonExist() { + // given + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() + .placeName("잠실한강공원") + .todos(Collections.EMPTY_LIST) + .position(locationRequest) + .build(); + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayRequest)) + .build(); + + // when & then + assertThatThrownBy(() -> travelPlanService.updateTravelPlan(1L, memberAuth, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 여행 계획입니다."); + } + + @DisplayName("여행 계획 서비스는 작성자가 아닌 사용자가 수정 시 예외를 반환한다.") + @Test + void updateTravelPlanWithNotAuthor() { + // given + Long id = testHelper.initTravelPlanTestData(author).getId(); + MemberAuth notAuthor = new MemberAuth(testHelper.initMemberTestData().getId()); + PlanPositionRequest locationRequest = new PlanPositionRequest("37.5175896", "127.0867236"); + PlanPlaceRequest planPlaceRequest = PlanPlaceRequest.builder() + .placeName("잠실한강공원") + .todos(Collections.EMPTY_LIST) + .position(locationRequest) + .build(); + PlanDayRequest planDayRequest = new PlanDayRequest(List.of(planPlaceRequest)); + PlanRequest request = PlanRequest.builder() + .title("신나는 한강 여행") + .startDate(LocalDate.MAX) + .days(List.of(planDayRequest)) + .build(); + + // when & then + assertThatThrownBy(() -> travelPlanService.updateTravelPlan(id, notAuthor, request)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("여행 계획 수정은 작성자만 가능합니다."); + } + @DisplayName("여행계획을 ID 기준으로 삭제할 수 있다.") @Test void deleteTravelPlanById() { From 27e1c4b9cbd61a6c3da5a146b24ae334882d11f1 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 20 Aug 2024 09:59:51 +0900 Subject: [PATCH 104/108] =?UTF-8?q?[Fix]=20-=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=BF=BC=EB=A6=AC=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../travelogue/repository/TraveloguePhotoRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java index d25b3c79..c60e1384 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -13,6 +13,6 @@ public interface TraveloguePhotoRepository extends JpaRepository findByTraveloguePlace(TraveloguePlace traveloguePlace); @Modifying(flushAutomatically = true, clearAutomatically = true) - @Query("UPDATE TraveloguePhoto tp SET tp.deletedAt = NOW() WHERE tp.traveloguePlace.travelogueDay.travelogue = :travelogue") + @Query("UPDATE TraveloguePhoto to SET to.deletedAt = NOW() WHERE to.traveloguePlace.travelogueDay.travelogue = :travelogue") void deleteAllByTraveloguePlaceTravelogueDayTravelogue(Travelogue travelogue); } From 732dd19ccde935ded75c499a2d3eaf13f9f73cff Mon Sep 17 00:00:00 2001 From: eunjungL <62099953+eunjungL@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:28:34 +0900 Subject: [PATCH 105/108] =?UTF-8?q?[Feature]=20-=20Spring=20boot=20Prod=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EB=B6=84=EB=A6=AC=20(#345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: prod profile용 db 정보, jwt 정보 추가 * refactor: Dockerfile profile 지정 제거 * feat: prod profile logging 추가 * feat: prod profile swagger 비활성화 * feat: prod profile ssl 설정 변경 --- backend/Dockerfile | 2 +- .../touroot/global/config/SwaggerConfig.java | 2 + backend/src/main/resources/application.yml | 87 ++++++++++++++----- backend/src/main/resources/logback-spring.xml | 9 ++ 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 310ffcfa..2e05cbe0 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ FROM openjdk:17-oracle ARG JAR_FILE=build/libs/*.jar COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java", "-jar", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=dev", "/app.jar"] +ENTRYPOINT ["java", "-jar", "-Duser.timezone=Asia/Seoul", "/app.jar"] diff --git a/backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java b/backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java index 23525c21..a52d3636 100644 --- a/backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java +++ b/backend/src/main/java/kr/touroot/global/config/SwaggerConfig.java @@ -7,8 +7,10 @@ import io.swagger.v3.oas.models.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; +@Profile("local|dev") @Configuration public class SwaggerConfig { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index cffb9098..910665cb 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -3,10 +3,12 @@ oauth: user-information-request-uri: https://kapi.kakao.com/v2/user/me access-token-request-uri: https://kauth.kakao.com/oauth/token rest-api-key: ENC(i6J7NWUsDpXVbJrbSUcNFI3h0oc6v8PxuHShU9UA7EVuUNLtQN/ANII+8j5HjhGO) + jasypt: encryptor: algorithm: PBEWithMD5AndDES iv-generator-classname: org.jasypt.iv.NoIvGenerator + cloud: aws: s3: @@ -17,14 +19,6 @@ cloud: image-storage-path: images/ --- # local profile -security: - jwt: - token: - secret-key: ENC(SNwFG2NQDZkmIK3nNoZFdwQ0ZxKuoe+qcw10ljdW941YEx/Qky9PEEl+wvAN9S1KR26D3a83SnU=) - expire-length: 1800000 # 30 min - refresh: - secret-key: ENC(tneEW6IKq9XuDoxAoKvBEVER4xjLHCycWXMa+Rnzb700ndTnrkJ2mOtBPP5hEIJLRNgj5MLIhYs=) - expire-length: 1209600000 # 14 days spring: config: activate: @@ -40,47 +34,100 @@ spring: show-sql: false properties: hibernate: - format_sql: true dialect: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop defer-datasource-initialization: true ---- -# dev profile + security: jwt: token: - secret-key: ENC(L36WWjoZtP2nHHkqxDGlYLsMHMp+EBL2Fnl+X2de2KHk+PIfViyVM7rCYcbcFpo7yB4MaP++atU=) + secret-key: ENC(SNwFG2NQDZkmIK3nNoZFdwQ0ZxKuoe+qcw10ljdW941YEx/Qky9PEEl+wvAN9S1KR26D3a83SnU=) expire-length: 1800000 # 30 min refresh: - secret-key: ENC(cDstTL4/ajLm3NohJwMR9vEBsIZeD9Vt+jE1obdwL8Q6gMnWvY3N+bmNsC9N0csaa6AaYIZLbFM=) + secret-key: ENC(tneEW6IKq9XuDoxAoKvBEVER4xjLHCycWXMa+Rnzb700ndTnrkJ2mOtBPP5hEIJLRNgj5MLIhYs=) expire-length: 1209600000 # 14 days +--- +# dev profile +spring: + config: + activate: + on-profile: dev + servlet: + multipart: + max-file-size: 25MB + max-request-size: 250MB + h2: + console: + enabled: false + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ENC(FgbVXdH4a5/EkMxdmYfPhFKAOwn1w+/CnmWDcN9p6fOVP6mP9coMEYezCPCNf95h) + username: ENC(SJznQPqjlZuw3qf8kv9IJQ==) + password: ENC(HsOo6wWp//egPPsSG6Wf40eF1Q2sVKfGuH4zGTL81Mw=) + jpa: + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: none + server: ssl: key-store-type: PKCS12 key-store-password: ENC(faQYah2QoIaNVRZD9J6/junPRWkc5gaiAs+mEbxDk+I=) key-store: ENC(7VQCNdI7mXATwc4AiymZoyf3mz9SiskXpLnenpMSFBI=) + +security: + jwt: + token: + secret-key: ENC(L36WWjoZtP2nHHkqxDGlYLsMHMp+EBL2Fnl+X2de2KHk+PIfViyVM7rCYcbcFpo7yB4MaP++atU=) + expire-length: 1800000 # 30 min + refresh: + secret-key: ENC(cDstTL4/ajLm3NohJwMR9vEBsIZeD9Vt+jE1obdwL8Q6gMnWvY3N+bmNsC9N0csaa6AaYIZLbFM=) + expire-length: 1209600000 # 14 days +--- +# prod profile spring: + config: + activate: + on-profile: prod servlet: multipart: max-file-size: 25MB max-request-size: 250MB - config: - activate: - on-profile: dev h2: console: enabled: false datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ENC(FgbVXdH4a5/EkMxdmYfPhFKAOwn1w+/CnmWDcN9p6fOVP6mP9coMEYezCPCNf95h) - username: ENC(SJznQPqjlZuw3qf8kv9IJQ==) - password: ENC(HsOo6wWp//egPPsSG6Wf40eF1Q2sVKfGuH4zGTL81Mw=) + url: ENC(BLlw5HyZCuuLnw9MN5Ez395f+9INR7KoyWKUArAGc5QuMNJw07P06/1HLSZZ6y8M) + username: ENC(s5TCLskHnyopzJBsWU9Akg==) + password: ENC(MlRZxamsKXaRANlE3dX9T3vrdJhtsoE0r6LvSaKZoSU=) jpa: show-sql: false properties: hibernate: - format_sql: true dialect: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: none + +server: + ssl: + key-store-type: PKCS12 + key-store-password: ENC(WyJqMojCOPDe932QhOR52Ll0EhthbUe5ycHs0WWifQg=) + key-store: ENC(DtVtAx+kVnU581+bjT7A3vCLbJ8fe3JqUritO+0WJ/Q=) + +security: + jwt: + token: + secret-key: ENC(1oFbXL7gi5RoZ3477IE75WnKK6t/Mq18IRzKB4TjRI/kb5ViYqbkBesMhPzjakFnAeqwiIWg0cQ=) + expire-length: 1800000 # 30 min + refresh: + secret-key: ENC(KzrDzCSz4dIMMP6Vsyd9cYvOvGXMbetrCDAX0/IpHfVW7kdsbV2+ZlGcBz+RCS2whMPuoCxdhyE=) + expire-length: 1209600000 # 14 days + +springdoc: + api-docs: + enabled: false diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 1e140d89..134165e8 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -84,4 +84,13 @@ + + + + + + + + + From ab0594dff409fa6da712fdad78dd473977966ff3 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 20 Aug 2024 14:06:22 +0900 Subject: [PATCH 106/108] =?UTF-8?q?[Fix]=20-=20JPQL=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20alias=20ambiguous=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=20(#352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 중복되는 table alias 변경 * fix: 예외 발생 중인 JPQL을 대체하는 QueryDSL 구현 * fix: 여행기 삭제 및 수정 기능 쿼리 수정 --- .../repository/TraveloguePhotoRepository.java | 7 ----- .../repository/TraveloguePlaceRepository.java | 7 ----- .../repository/TravelogueTagRepository.java | 6 ---- .../query/TravelogueDayQueryRepository.java | 8 +++++ .../TravelogueDayQueryRepositoryImpl.java | 24 ++++++++++++++ .../query/TraveloguePhotoQueryRepository.java | 8 +++++ .../TraveloguePhotoQueryRepositoryImpl.java | 31 +++++++++++++++++++ .../query/TraveloguePlaceQueryRepository.java | 8 +++++ .../TraveloguePlaceQueryRepositoryImpl.java | 24 ++++++++++++++ .../query/TravelogueTagQueryRepository.java | 8 +++++ .../TravelogueTagQueryRepositoryImpl.java | 22 +++++++++++++ .../service/TravelogueDayService.java | 4 ++- .../service/TraveloguePhotoService.java | 4 ++- .../service/TraveloguePlaceService.java | 4 ++- .../service/TravelogueTagService.java | 4 ++- .../global/config/TestQueryDslConfig.java | 28 +++++++++++++++++ .../touroot/tag/service/TagServiceTest.java | 3 +- .../service/TravelogueDayServiceTest.java | 3 +- .../service/TraveloguePhotoServiceTest.java | 4 ++- .../service/TraveloguePlaceServiceTest.java | 3 +- 20 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepository.java create mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepositoryImpl.java diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java index c60e1384..b3101d76 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -1,18 +1,11 @@ package kr.touroot.travelogue.repository; import java.util.List; -import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; public interface TraveloguePhotoRepository extends JpaRepository { List findByTraveloguePlace(TraveloguePlace traveloguePlace); - - @Modifying(flushAutomatically = true, clearAutomatically = true) - @Query("UPDATE TraveloguePhoto to SET to.deletedAt = NOW() WHERE to.traveloguePlace.travelogueDay.travelogue = :travelogue") - void deleteAllByTraveloguePlaceTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java index 3f06bd06..00cfb870 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java @@ -1,18 +1,11 @@ package kr.touroot.travelogue.repository; import java.util.List; -import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePlace; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; public interface TraveloguePlaceRepository extends JpaRepository { List findByTravelogueDay(TravelogueDay travelogueDay); - - @Modifying(flushAutomatically = true, clearAutomatically = true) - @Query("UPDATE TraveloguePlace tp SET tp.deletedAt = NOW() WHERE tp.travelogueDay.travelogue = :travelogue") - void deleteAllByTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java index d36ed1fc..2f3f6fb7 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueTagRepository.java @@ -4,14 +4,8 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueTag; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; public interface TravelogueTagRepository extends JpaRepository { List findAllByTravelogue(Travelogue travelogue); - - @Modifying(flushAutomatically = true, clearAutomatically = true) - @Query("DELETE TravelogueTag tt WHERE tt.travelogue = :travelogue") - void deleteAllByTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java new file mode 100644 index 00000000..6508fe9f --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java @@ -0,0 +1,8 @@ +package kr.touroot.travelogue.repository.query; + +import kr.touroot.travelogue.domain.Travelogue; + +public interface TravelogueDayQueryRepository { + + void deleteAllByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java new file mode 100644 index 00000000..777434b6 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java @@ -0,0 +1,24 @@ +package kr.touroot.travelogue.repository.query; + +import static kr.touroot.travelogue.domain.QTravelogueDay.travelogueDay; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import kr.touroot.travelogue.domain.Travelogue; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class TravelogueDayQueryRepositoryImpl implements TravelogueDayQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public void deleteAllByTravelogue(Travelogue travelogue) { + jpaQueryFactory.update(travelogueDay) + .set(travelogueDay.deletedAt, LocalDateTime.now()) + .where(travelogueDay.travelogue.eq(travelogue)) + .execute(); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java new file mode 100644 index 00000000..c97205df --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java @@ -0,0 +1,8 @@ +package kr.touroot.travelogue.repository.query; + +import kr.touroot.travelogue.domain.Travelogue; + +public interface TraveloguePhotoQueryRepository { + + void deleteAllByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java new file mode 100644 index 00000000..607b184e --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java @@ -0,0 +1,31 @@ +package kr.touroot.travelogue.repository.query; + +import static kr.touroot.travelogue.domain.QTravelogueDay.travelogueDay; +import static kr.touroot.travelogue.domain.QTraveloguePhoto.traveloguePhoto; + +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import kr.touroot.travelogue.domain.QTravelogue; +import kr.touroot.travelogue.domain.Travelogue; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class TraveloguePhotoQueryRepositoryImpl implements TraveloguePhotoQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public void deleteAllByTravelogue(Travelogue travelogue) { + jpaQueryFactory.update(traveloguePhoto) + .set(traveloguePhoto.deletedAt, LocalDateTime.now()) + .where(traveloguePhoto.traveloguePlace.travelogueDay.eq( + JPAExpressions.selectFrom(travelogueDay) + .leftJoin(QTravelogue.travelogue) + .on(QTravelogue.travelogue.eq(travelogue)) + )) + .execute(); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java new file mode 100644 index 00000000..987cedba --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java @@ -0,0 +1,8 @@ +package kr.touroot.travelogue.repository.query; + +import kr.touroot.travelogue.domain.Travelogue; + +public interface TraveloguePlaceQueryRepository { + + void deleteAllByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java new file mode 100644 index 00000000..149bb5d3 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java @@ -0,0 +1,24 @@ +package kr.touroot.travelogue.repository.query; + +import static kr.touroot.travelogue.domain.QTraveloguePlace.traveloguePlace; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import kr.touroot.travelogue.domain.Travelogue; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class TraveloguePlaceQueryRepositoryImpl implements TraveloguePlaceQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public void deleteAllByTravelogue(Travelogue travelogue) { + jpaQueryFactory.update(traveloguePlace) + .set(traveloguePlace.deletedAt, LocalDateTime.now()) + .where(traveloguePlace.travelogueDay.travelogue.eq(travelogue)) + .execute(); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepository.java new file mode 100644 index 00000000..dcf4aa09 --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepository.java @@ -0,0 +1,8 @@ +package kr.touroot.travelogue.repository.query; + +import kr.touroot.travelogue.domain.Travelogue; + +public interface TravelogueTagQueryRepository { + + void deleteAllByTravelogue(Travelogue travelogue); +} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepositoryImpl.java new file mode 100644 index 00000000..9ba91e0c --- /dev/null +++ b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueTagQueryRepositoryImpl.java @@ -0,0 +1,22 @@ +package kr.touroot.travelogue.repository.query; + +import static kr.touroot.travelogue.domain.QTravelogueTag.travelogueTag; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import kr.touroot.travelogue.domain.Travelogue; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class TravelogueTagQueryRepositoryImpl implements TravelogueTagQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public void deleteAllByTravelogue(Travelogue travelogue) { + jpaQueryFactory.delete(travelogueTag) + .where(travelogueTag.travelogue.eq(travelogue)) + .execute(); + } +} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java index 146c70d1..fcd317d4 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java @@ -9,6 +9,7 @@ import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.repository.TravelogueDayRepository; +import kr.touroot.travelogue.repository.query.TravelogueDayQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +19,7 @@ public class TravelogueDayService { private final TravelogueDayRepository travelogueDayRepository; + private final TravelogueDayQueryRepository travelogueDayQueryRepository; @Transactional public Map> createDays( @@ -48,6 +50,6 @@ public TravelogueDay findDayById(Long id) { @Transactional public void deleteAllByTravelogue(Travelogue travelogue) { - travelogueDayRepository.deleteAllByTravelogue(travelogue); + travelogueDayQueryRepository.deleteAllByTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java index 1ac3d17d..c4c6e993 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java @@ -9,6 +9,7 @@ import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; +import kr.touroot.travelogue.repository.query.TraveloguePhotoQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +20,7 @@ public class TraveloguePhotoService { private final TraveloguePhotoRepository traveloguePhotoRepository; private final AwsS3Provider s3Provider; + private final TraveloguePhotoQueryRepository traveloguePhotoQueryRepository; @Transactional public List createPhotos(List requests, TraveloguePlace place) { @@ -46,6 +48,6 @@ public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { @Transactional public void deleteAllByTravelogue(Travelogue travelogue) { - traveloguePhotoRepository.deleteAllByTraveloguePlaceTravelogueDayTravelogue(travelogue); + traveloguePhotoQueryRepository.deleteAllByTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java index ca7f2951..f9bc080d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java @@ -12,6 +12,7 @@ import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.repository.TraveloguePlaceRepository; +import kr.touroot.travelogue.repository.query.TraveloguePlaceQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +23,7 @@ public class TraveloguePlaceService { private final PlaceRepository placeRepository; private final TraveloguePlaceRepository traveloguePlaceRepository; + private final TraveloguePlaceQueryRepository traveloguePlaceQueryRepository; @Transactional public Map> createPlaces( @@ -62,6 +64,6 @@ public TraveloguePlace findTraveloguePlaceById(Long id) { @Transactional public void deleteAllByTravelogue(Travelogue travelogue) { - traveloguePlaceRepository.deleteAllByTravelogueDayTravelogue(travelogue); + traveloguePlaceQueryRepository.deleteAllByTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java index 7b441eca..cc07d0bd 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueTagService.java @@ -8,6 +8,7 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueTag; import kr.touroot.travelogue.repository.TravelogueTagRepository; +import kr.touroot.travelogue.repository.query.TravelogueTagQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +19,7 @@ public class TravelogueTagService { private final TagRepository tagRepository; private final TravelogueTagRepository travelogueTagRepository; + private final TravelogueTagQueryRepository travelogueTagQueryRepository; @Transactional public List createTravelogueTags(Travelogue travelogue, List tagIds) { @@ -43,7 +45,7 @@ public List readTagByTravelogue(Travelogue travelogue) { @Transactional(readOnly = true) public List updateTravelogueTags(Travelogue travelogue, List tagIds) { - travelogueTagRepository.deleteAllByTravelogue(travelogue); + travelogueTagQueryRepository.deleteAllByTravelogue(travelogue); return tagIds.stream() .map(id -> { diff --git a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java index 838d7964..9006757c 100644 --- a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java +++ b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java @@ -3,8 +3,16 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import kr.touroot.travelogue.repository.query.TravelogueDayQueryRepository; +import kr.touroot.travelogue.repository.query.TravelogueDayQueryRepositoryImpl; +import kr.touroot.travelogue.repository.query.TraveloguePhotoQueryRepository; +import kr.touroot.travelogue.repository.query.TraveloguePhotoQueryRepositoryImpl; +import kr.touroot.travelogue.repository.query.TraveloguePlaceQueryRepository; +import kr.touroot.travelogue.repository.query.TraveloguePlaceQueryRepositoryImpl; import kr.touroot.travelogue.repository.query.TravelogueQueryRepository; import kr.touroot.travelogue.repository.query.TravelogueQueryRepositoryImpl; +import kr.touroot.travelogue.repository.query.TravelogueTagQueryRepository; +import kr.touroot.travelogue.repository.query.TravelogueTagQueryRepositoryImpl; import kr.touroot.travelplan.repository.PlaceTodoQueryRepository; import kr.touroot.travelplan.repository.PlaceTodoQueryRepositoryImpl; import lombok.RequiredArgsConstructor; @@ -28,6 +36,26 @@ public TravelogueQueryRepository travelogueQueryRepository() { return new TravelogueQueryRepositoryImpl(jpaQueryFactory()); } + @Bean + public TravelogueDayQueryRepository travelogueDayQueryRepository() { + return new TravelogueDayQueryRepositoryImpl(jpaQueryFactory()); + } + + @Bean + public TraveloguePlaceQueryRepository traveloguePlaceQueryRepository() { + return new TraveloguePlaceQueryRepositoryImpl(jpaQueryFactory()); + } + + @Bean + public TraveloguePhotoQueryRepository traveloguePhotoQueryRepository() { + return new TraveloguePhotoQueryRepositoryImpl(jpaQueryFactory()); + } + + @Bean + public TravelogueTagQueryRepository travelogueTagQueryRepository() { + return new TravelogueTagQueryRepositoryImpl(jpaQueryFactory()); + } + @Bean public PlaceTodoQueryRepository placeTodoQueryRepository() { return new PlaceTodoQueryRepositoryImpl(jpaQueryFactory()); diff --git a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java index 117b3fd4..5e30d5d0 100644 --- a/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java +++ b/backend/src/test/java/kr/touroot/tag/service/TagServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import kr.touroot.global.ServiceTest; +import kr.touroot.global.config.TestQueryDslConfig; import kr.touroot.global.exception.BadRequestException; import kr.touroot.tag.domain.Tag; import kr.touroot.tag.dto.TagCreateRequest; @@ -16,7 +17,7 @@ import org.springframework.context.annotation.Import; @DisplayName("태그 서비스") -@Import(value = {TagService.class, TagTestHelper.class}) +@Import(value = {TagService.class, TagTestHelper.class, TestQueryDslConfig.class}) @ServiceTest class TagServiceTest { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java index 27a589b3..80b41d57 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TravelogueDayServiceTest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import kr.touroot.global.ServiceTest; +import kr.touroot.global.config.TestQueryDslConfig; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import kr.touroot.travelogue.domain.Travelogue; @@ -26,7 +27,7 @@ import org.springframework.context.annotation.Import; @DisplayName("여행기 일자 서비스") -@Import(value = {TravelogueDayService.class, TravelogueTestHelper.class}) +@Import(value = {TravelogueDayService.class, TravelogueTestHelper.class, TestQueryDslConfig.class}) @ServiceTest class TravelogueDayServiceTest { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java index 230fff22..7a341389 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePhotoServiceTest.java @@ -5,6 +5,7 @@ import java.util.List; import kr.touroot.global.ServiceTest; +import kr.touroot.global.config.TestQueryDslConfig; import kr.touroot.image.infrastructure.AwsS3Provider; import kr.touroot.member.domain.Member; import kr.touroot.place.domain.Place; @@ -26,7 +27,8 @@ import org.springframework.context.annotation.Import; @DisplayName("여행기 사진 서비스") -@Import(value = {TraveloguePhotoService.class, TravelogueTestHelper.class, AwsS3Provider.class}) +@Import(value = {TraveloguePhotoService.class, TravelogueTestHelper.class, AwsS3Provider.class, + TestQueryDslConfig.class}) @ServiceTest class TraveloguePhotoServiceTest { diff --git a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java index 4f5c8f12..0e18cb30 100644 --- a/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java +++ b/backend/src/test/java/kr/touroot/travelogue/service/TraveloguePlaceServiceTest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import kr.touroot.global.ServiceTest; +import kr.touroot.global.config.TestQueryDslConfig; import kr.touroot.global.exception.BadRequestException; import kr.touroot.member.domain.Member; import kr.touroot.place.domain.Place; @@ -27,7 +28,7 @@ import org.springframework.context.annotation.Import; @DisplayName("여행기 장소 서비스") -@Import(value = {TraveloguePlaceService.class, TravelogueTestHelper.class}) +@Import(value = {TraveloguePlaceService.class, TravelogueTestHelper.class, TestQueryDslConfig.class}) @ServiceTest class TraveloguePlaceServiceTest { From 3a1e291e1dddc64349d8db216e20740b86f1a2d7 Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 20 Aug 2024 15:41:15 +0900 Subject: [PATCH 107/108] =?UTF-8?q?[Fix]=20-=20JPQL=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20alias=20ambiguous=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=20(#355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 중복되는 table alias 변경 * fix: 예외 발생 중인 JPQL을 대체하는 QueryDSL 구현 * fix: 여행기 삭제 및 수정 기능 쿼리 수정 * fix: 여행기 삭제 및 수정 기능 수정 * fix: conflict 해결 --- .../repository/TravelogueDayRepository.java | 4 --- .../repository/TraveloguePhotoRepository.java | 3 ++ .../repository/TraveloguePlaceRepository.java | 3 ++ .../query/TravelogueDayQueryRepository.java | 8 ----- .../TravelogueDayQueryRepositoryImpl.java | 24 -------------- .../query/TraveloguePhotoQueryRepository.java | 8 ----- .../TraveloguePhotoQueryRepositoryImpl.java | 31 ------------------- .../query/TraveloguePlaceQueryRepository.java | 8 ----- .../TraveloguePlaceQueryRepositoryImpl.java | 24 -------------- .../service/TravelogueDayService.java | 4 +-- .../service/TraveloguePhotoService.java | 4 +-- .../service/TraveloguePlaceService.java | 4 +-- .../global/config/TestQueryDslConfig.java | 21 ------------- 13 files changed, 9 insertions(+), 137 deletions(-) delete mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java delete mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java delete mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java delete mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java delete mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java delete mode 100644 backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java index bcc5f0c3..f1f65faf 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TravelogueDayRepository.java @@ -4,14 +4,10 @@ import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; public interface TravelogueDayRepository extends JpaRepository { List findByTravelogue(Travelogue travelogue); - @Modifying(flushAutomatically = true, clearAutomatically = true) - @Query("UPDATE TravelogueDay td SET td.deletedAt = NOW() WHERE td.travelogue = :travelogue") void deleteAllByTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java index b3101d76..c9324a7e 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePhotoRepository.java @@ -1,6 +1,7 @@ package kr.touroot.travelogue.repository; import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TraveloguePhoto; import kr.touroot.travelogue.domain.TraveloguePlace; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,4 +9,6 @@ public interface TraveloguePhotoRepository extends JpaRepository { List findByTraveloguePlace(TraveloguePlace traveloguePlace); + + void deleteAllByTraveloguePlaceTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java index 00cfb870..697f538c 100644 --- a/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java +++ b/backend/src/main/java/kr/touroot/travelogue/repository/TraveloguePlaceRepository.java @@ -1,6 +1,7 @@ package kr.touroot.travelogue.repository; import java.util.List; +import kr.touroot.travelogue.domain.Travelogue; import kr.touroot.travelogue.domain.TravelogueDay; import kr.touroot.travelogue.domain.TraveloguePlace; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,4 +9,6 @@ public interface TraveloguePlaceRepository extends JpaRepository { List findByTravelogueDay(TravelogueDay travelogueDay); + + void deleteAllByTravelogueDayTravelogue(Travelogue travelogue); } diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java deleted file mode 100644 index 6508fe9f..00000000 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package kr.touroot.travelogue.repository.query; - -import kr.touroot.travelogue.domain.Travelogue; - -public interface TravelogueDayQueryRepository { - - void deleteAllByTravelogue(Travelogue travelogue); -} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java deleted file mode 100644 index 777434b6..00000000 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TravelogueDayQueryRepositoryImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package kr.touroot.travelogue.repository.query; - -import static kr.touroot.travelogue.domain.QTravelogueDay.travelogueDay; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.time.LocalDateTime; -import kr.touroot.travelogue.domain.Travelogue; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@RequiredArgsConstructor -@Repository -public class TravelogueDayQueryRepositoryImpl implements TravelogueDayQueryRepository { - - private final JPAQueryFactory jpaQueryFactory; - - @Override - public void deleteAllByTravelogue(Travelogue travelogue) { - jpaQueryFactory.update(travelogueDay) - .set(travelogueDay.deletedAt, LocalDateTime.now()) - .where(travelogueDay.travelogue.eq(travelogue)) - .execute(); - } -} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java deleted file mode 100644 index c97205df..00000000 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package kr.touroot.travelogue.repository.query; - -import kr.touroot.travelogue.domain.Travelogue; - -public interface TraveloguePhotoQueryRepository { - - void deleteAllByTravelogue(Travelogue travelogue); -} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java deleted file mode 100644 index 607b184e..00000000 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePhotoQueryRepositoryImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package kr.touroot.travelogue.repository.query; - -import static kr.touroot.travelogue.domain.QTravelogueDay.travelogueDay; -import static kr.touroot.travelogue.domain.QTraveloguePhoto.traveloguePhoto; - -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.time.LocalDateTime; -import kr.touroot.travelogue.domain.QTravelogue; -import kr.touroot.travelogue.domain.Travelogue; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@RequiredArgsConstructor -@Repository -public class TraveloguePhotoQueryRepositoryImpl implements TraveloguePhotoQueryRepository { - - private final JPAQueryFactory jpaQueryFactory; - - @Override - public void deleteAllByTravelogue(Travelogue travelogue) { - jpaQueryFactory.update(traveloguePhoto) - .set(traveloguePhoto.deletedAt, LocalDateTime.now()) - .where(traveloguePhoto.traveloguePlace.travelogueDay.eq( - JPAExpressions.selectFrom(travelogueDay) - .leftJoin(QTravelogue.travelogue) - .on(QTravelogue.travelogue.eq(travelogue)) - )) - .execute(); - } -} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java deleted file mode 100644 index 987cedba..00000000 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package kr.touroot.travelogue.repository.query; - -import kr.touroot.travelogue.domain.Travelogue; - -public interface TraveloguePlaceQueryRepository { - - void deleteAllByTravelogue(Travelogue travelogue); -} diff --git a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java b/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java deleted file mode 100644 index 149bb5d3..00000000 --- a/backend/src/main/java/kr/touroot/travelogue/repository/query/TraveloguePlaceQueryRepositoryImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package kr.touroot.travelogue.repository.query; - -import static kr.touroot.travelogue.domain.QTraveloguePlace.traveloguePlace; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.time.LocalDateTime; -import kr.touroot.travelogue.domain.Travelogue; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -@RequiredArgsConstructor -@Repository -public class TraveloguePlaceQueryRepositoryImpl implements TraveloguePlaceQueryRepository { - - private final JPAQueryFactory jpaQueryFactory; - - @Override - public void deleteAllByTravelogue(Travelogue travelogue) { - jpaQueryFactory.update(traveloguePlace) - .set(traveloguePlace.deletedAt, LocalDateTime.now()) - .where(traveloguePlace.travelogueDay.travelogue.eq(travelogue)) - .execute(); - } -} diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java index fcd317d4..146c70d1 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TravelogueDayService.java @@ -9,7 +9,6 @@ import kr.touroot.travelogue.dto.request.TravelogueDayRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.repository.TravelogueDayRepository; -import kr.touroot.travelogue.repository.query.TravelogueDayQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,7 +18,6 @@ public class TravelogueDayService { private final TravelogueDayRepository travelogueDayRepository; - private final TravelogueDayQueryRepository travelogueDayQueryRepository; @Transactional public Map> createDays( @@ -50,6 +48,6 @@ public TravelogueDay findDayById(Long id) { @Transactional public void deleteAllByTravelogue(Travelogue travelogue) { - travelogueDayQueryRepository.deleteAllByTravelogue(travelogue); + travelogueDayRepository.deleteAllByTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java index c4c6e993..1ac3d17d 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePhotoService.java @@ -9,7 +9,6 @@ import kr.touroot.travelogue.domain.TraveloguePlace; import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.repository.TraveloguePhotoRepository; -import kr.touroot.travelogue.repository.query.TraveloguePhotoQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,7 +19,6 @@ public class TraveloguePhotoService { private final TraveloguePhotoRepository traveloguePhotoRepository; private final AwsS3Provider s3Provider; - private final TraveloguePhotoQueryRepository traveloguePhotoQueryRepository; @Transactional public List createPhotos(List requests, TraveloguePlace place) { @@ -48,6 +46,6 @@ public List findPhotoUrlsByPlace(TraveloguePlace traveloguePlace) { @Transactional public void deleteAllByTravelogue(Travelogue travelogue) { - traveloguePhotoQueryRepository.deleteAllByTravelogue(travelogue); + traveloguePhotoRepository.deleteAllByTraveloguePlaceTravelogueDayTravelogue(travelogue); } } diff --git a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java index f9bc080d..ca7f2951 100644 --- a/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java +++ b/backend/src/main/java/kr/touroot/travelogue/service/TraveloguePlaceService.java @@ -12,7 +12,6 @@ import kr.touroot.travelogue.dto.request.TraveloguePhotoRequest; import kr.touroot.travelogue.dto.request.TraveloguePlaceRequest; import kr.touroot.travelogue.repository.TraveloguePlaceRepository; -import kr.touroot.travelogue.repository.query.TraveloguePlaceQueryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,7 +22,6 @@ public class TraveloguePlaceService { private final PlaceRepository placeRepository; private final TraveloguePlaceRepository traveloguePlaceRepository; - private final TraveloguePlaceQueryRepository traveloguePlaceQueryRepository; @Transactional public Map> createPlaces( @@ -64,6 +62,6 @@ public TraveloguePlace findTraveloguePlaceById(Long id) { @Transactional public void deleteAllByTravelogue(Travelogue travelogue) { - traveloguePlaceQueryRepository.deleteAllByTravelogue(travelogue); + traveloguePlaceRepository.deleteAllByTravelogueDayTravelogue(travelogue); } } diff --git a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java index 9006757c..60d308ba 100644 --- a/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java +++ b/backend/src/test/java/kr/touroot/global/config/TestQueryDslConfig.java @@ -3,12 +3,6 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; -import kr.touroot.travelogue.repository.query.TravelogueDayQueryRepository; -import kr.touroot.travelogue.repository.query.TravelogueDayQueryRepositoryImpl; -import kr.touroot.travelogue.repository.query.TraveloguePhotoQueryRepository; -import kr.touroot.travelogue.repository.query.TraveloguePhotoQueryRepositoryImpl; -import kr.touroot.travelogue.repository.query.TraveloguePlaceQueryRepository; -import kr.touroot.travelogue.repository.query.TraveloguePlaceQueryRepositoryImpl; import kr.touroot.travelogue.repository.query.TravelogueQueryRepository; import kr.touroot.travelogue.repository.query.TravelogueQueryRepositoryImpl; import kr.touroot.travelogue.repository.query.TravelogueTagQueryRepository; @@ -36,21 +30,6 @@ public TravelogueQueryRepository travelogueQueryRepository() { return new TravelogueQueryRepositoryImpl(jpaQueryFactory()); } - @Bean - public TravelogueDayQueryRepository travelogueDayQueryRepository() { - return new TravelogueDayQueryRepositoryImpl(jpaQueryFactory()); - } - - @Bean - public TraveloguePlaceQueryRepository traveloguePlaceQueryRepository() { - return new TraveloguePlaceQueryRepositoryImpl(jpaQueryFactory()); - } - - @Bean - public TraveloguePhotoQueryRepository traveloguePhotoQueryRepository() { - return new TraveloguePhotoQueryRepositoryImpl(jpaQueryFactory()); - } - @Bean public TravelogueTagQueryRepository travelogueTagQueryRepository() { return new TravelogueTagQueryRepositoryImpl(jpaQueryFactory()); From db3dae0519f92e4164d84f44049439fcf0ad89ad Mon Sep 17 00:00:00 2001 From: Lee Hangil Date: Tue, 20 Aug 2024 16:32:31 +0900 Subject: [PATCH 108/108] Create be-prod-cd.yml --- .github/workflows/be-prod-cd.yml | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/be-prod-cd.yml diff --git a/.github/workflows/be-prod-cd.yml b/.github/workflows/be-prod-cd.yml new file mode 100644 index 00000000..4b986d7b --- /dev/null +++ b/.github/workflows/be-prod-cd.yml @@ -0,0 +1,67 @@ +name: BE CD PROD + +on: + push: + branches: [ production/be ] + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Make keystore file + run: echo "${{secrets.PROD_SSL_KEYSTORE}}" | base64 --decode > ./src/main/resources/keystore.p12 + + - name: Gradle Caching + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{runner.os}}-gradle-${{hashFiles('**/*.gradle*', '**/gradle-wrapper.properties')}} + restore-keys: | + ${{runner.os}}-gradle- + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew clean build + + - name: Sign in Dockerhub + uses: docker/login-action@v1 + with: + username: ${{secrets.DOCKER_USERNAME}} + password: ${{secrets.DOCKER_PASSWORD}} + + - name: Build the Docker image + run: docker build -f ./Dockerfile --platform linux/arm64 --no-cache -t touroot/touroot-api:prod . + + - name: Push the Docker Image to Dockerhub + run: docker push touroot/touroot-api:prod + + deploy: + needs: build + runs-on: [self-hosted, prod] + + steps: + - name: Docker Image pull + run: sudo docker pull touroot/touroot-api:prod + + - name: Docker Compose up + run: sudo docker compose -f ~/docker/api-docker.yml up -d