From f974e3ecac8575271e1955cb41534242c29a667c Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:14:22 +0900 Subject: [PATCH 01/97] =?UTF-8?q?[Init]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=B4=88=EA=B8=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 39 ++++ blacklist.csv | 2 + build.gradle | 27 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 +++++++++++++++++++++++ gradlew.bat | 92 +++++++++ settings.gradle | 1 + voucher.csv | 2 + 9 files changed, 419 insertions(+) create mode 100644 .gitignore create mode 100644 blacklist.csv create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 voucher.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..caf5bfb169 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +logs \ No newline at end of file diff --git a/blacklist.csv b/blacklist.csv new file mode 100644 index 0000000000..2b39d63eb8 --- /dev/null +++ b/blacklist.csv @@ -0,0 +1,2 @@ +id,name +1,주환 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..566788c652 --- /dev/null +++ b/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.4' + id 'io.spring.dependency-management' version '1.1.3' +} + +group = 'com.programmers' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + implementation 'com.opencsv:opencsv:5.8' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7f93135c49b765f8051ef9d0a6055ff8e46073d8 GIT binary patch literal 63721 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鯊%1Et zwk~hMkhq;MOw^Q5`7oC{CUUyTw9x>^%*FHx^qJw(LB+E0WBX@{Ghw;)6aA-KyYg8p z7XDveQOpEr;B4je@2~usI5BlFadedX^ma{b{ypd|RNYqo#~d*mj&y`^iojR}s%~vF z(H!u`yx68D1Tj(3(m;Q+Ma}s2n#;O~bcB1`lYk%Irx60&-nWIUBr2x&@}@76+*zJ5 ze&4?q8?m%L9c6h=J$WBzbiTf1Z-0Eb5$IZs>lvm$>1n_Mezp*qw_pr8<8$6f)5f<@ zyV#tzMCs51nTv_5ca`x`yfE5YA^*%O_H?;tWYdM_kHPubA%vy47i=9>Bq) zRQ&0UwLQHeswmB1yP)+BiR;S+Vc-5TX84KUA;8VY9}yEj0eESSO`7HQ4lO z4(CyA8y1G7_C;6kd4U3K-aNOK!sHE}KL_-^EDl(vB42P$2Km7$WGqNy=%fqB+ zSLdrlcbEH=T@W8V4(TgoXZ*G1_aq$K^@ek=TVhoKRjw;HyI&coln|uRr5mMOy2GXP zwr*F^Y|!Sjr2YQXX(Fp^*`Wk905K%$bd03R4(igl0&7IIm*#f`A!DCarW9$h$z`kYk9MjjqN&5-DsH@8xh63!fTNPxWsFQhNv z#|3RjnP$Thdb#Ys7M+v|>AHm0BVTw)EH}>x@_f4zca&3tXJhTZ8pO}aN?(dHo)44Z z_5j+YP=jMlFqwvf3lq!57-SAuRV2_gJ*wsR_!Y4Z(trO}0wmB9%f#jNDHPdQGHFR; zZXzS-$`;7DQ5vF~oSgP3bNV$6Z(rwo6W(U07b1n3UHqml>{=6&-4PALATsH@Bh^W? z)ob%oAPaiw{?9HfMzpGb)@Kys^J$CN{uf*HX?)z=g`J(uK1YO^8~s1(ZIbG%Et(|q z$D@_QqltVZu9Py4R0Ld8!U|#`5~^M=b>fnHthzKBRr=i+w@0Vr^l|W;=zFT#PJ?*a zbC}G#It}rQP^Ait^W&aa6B;+0gNvz4cWUMzpv(1gvfw-X4xJ2Sv;mt;zb2Tsn|kSS zo*U9N?I{=-;a-OybL4r;PolCfiaL=y@o9{%`>+&FI#D^uy#>)R@b^1ue&AKKwuI*` zx%+6r48EIX6nF4o;>)zhV_8(IEX})NGU6Vs(yslrx{5fII}o3SMHW7wGtK9oIO4OM&@@ECtXSICLcPXoS|{;=_yj>hh*%hP27yZwOmj4&Lh z*Nd@OMkd!aKReoqNOkp5cW*lC)&C$P?+H3*%8)6HcpBg&IhGP^77XPZpc%WKYLX$T zsSQ$|ntaVVOoRat$6lvZO(G-QM5s#N4j*|N_;8cc2v_k4n6zx9c1L4JL*83F-C1Cn zaJhd;>rHXB%%ZN=3_o3&Qd2YOxrK~&?1=UuN9QhL$~OY-Qyg&})#ez*8NpQW_*a&kD&ANjedxT0Ar z<6r{eaVz3`d~+N~vkMaV8{F?RBVemN(jD@S8qO~L{rUw#=2a$V(7rLE+kGUZ<%pdr z?$DP|Vg#gZ9S}w((O2NbxzQ^zTot=89!0^~hE{|c9q1hVzv0?YC5s42Yx($;hAp*E zyoGuRyphQY{Q2ee0Xx`1&lv(l-SeC$NEyS~8iil3_aNlnqF_G|;zt#F%1;J)jnPT& z@iU0S;wHJ2$f!juqEzPZeZkjcQ+Pa@eERSLKsWf=`{R@yv7AuRh&ALRTAy z8=g&nxsSJCe!QLchJ=}6|LshnXIK)SNd zRkJNiqHwKK{SO;N5m5wdL&qK`v|d?5<4!(FAsDxR>Ky#0#t$8XCMptvNo?|SY?d8b z`*8dVBlXTUanlh6n)!EHf2&PDG8sXNAt6~u-_1EjPI1|<=33T8 zEnA00E!`4Ave0d&VVh0e>)Dc}=FfAFxpsC1u9ATfQ`-Cu;mhc8Z>2;uyXtqpLb7(P zd2F9<3cXS} znMg?{&8_YFTGRQZEPU-XPq55%51}RJpw@LO_|)CFAt62-_!u_Uq$csc+7|3+TV_!h z+2a7Yh^5AA{q^m|=KSJL+w-EWDBc&I_I1vOr^}P8i?cKMhGy$CP0XKrQzCheG$}G# zuglf8*PAFO8%xop7KSwI8||liTaQ9NCAFarr~psQt)g*pC@9bORZ>m`_GA`_K@~&% zijH0z;T$fd;-Liw8%EKZas>BH8nYTqsK7F;>>@YsE=Rqo?_8}UO-S#|6~CAW0Oz1} z3F(1=+#wrBJh4H)9jTQ_$~@#9|Bc1Pd3rAIA_&vOpvvbgDJOM(yNPhJJq2%PCcMaI zrbe~toYzvkZYQ{ea(Wiyu#4WB#RRN%bMe=SOk!CbJZv^m?Flo5p{W8|0i3`hI3Np# zvCZqY%o258CI=SGb+A3yJe~JH^i{uU`#U#fvSC~rWTq+K`E%J@ zasU07&pB6A4w3b?d?q}2=0rA#SA7D`X+zg@&zm^iA*HVi z009#PUH<%lk4z~p^l0S{lCJk1Uxi=F4e_DwlfHA`X`rv(|JqWKAA5nH+u4Da+E_p+ zVmH@lg^n4ixs~*@gm_dgQ&eDmE1mnw5wBz9Yg?QdZwF|an67Xd*x!He)Gc8&2!urh z4_uXzbYz-aX)X1>&iUjGp;P1u8&7TID0bTH-jCL&Xk8b&;;6p2op_=y^m@Nq*0{#o!!A;wNAFG@0%Z9rHo zcJs?Th>Ny6+hI`+1XoU*ED$Yf@9f91m9Y=#N(HJP^Y@ZEYR6I?oM{>&Wq4|v0IB(p zqX#Z<_3X(&{H+{3Tr|sFy}~=bv+l=P;|sBz$wk-n^R`G3p0(p>p=5ahpaD7>r|>pm zv;V`_IR@tvZreIuv2EM7ZQHhO+qUgw#kOs%*ekY^n|=1#x9&c;Ro&I~{rG-#_3ZB1 z?|9}IFdbP}^DneP*T-JaoYHt~r@EfvnPE5EKUwIxjPbsr$% zfWW83pgWST7*B(o=kmo)74$8UU)v0{@4DI+ci&%=#90}!CZz|rnH+Mz=HN~97G3~@ z;v5(9_2%eca(9iu@J@aqaMS6*$TMw!S>H(b z4(*B!|H|8&EuB%mITr~O?vVEf%(Gr)6E=>H~1VR z&1YOXluJSG1!?TnT)_*YmJ*o_Q@om~(GdrhI{$Fsx_zrkupc#y{DK1WOUR>tk>ZE) ziOLoBkhZZ?0Uf}cm>GsA>Rd6V8@JF)J*EQlQ<=JD@m<)hyElXR0`pTku*3MU`HJn| zIf7$)RlK^pW-$87U;431;Ye4Ie+l~_B3*bH1>*yKzn23cH0u(i5pXV! z4K?{3oF7ZavmmtTq((wtml)m6i)8X6ot_mrE-QJCW}Yn!(3~aUHYG=^fA<^~`e3yc z-NWTb{gR;DOUcK#zPbN^D*e=2eR^_!(!RKkiwMW@@yYtEoOp4XjOGgzi`;=8 zi3`Ccw1%L*y(FDj=C7Ro-V?q)-%p?Ob2ZElu`eZ99n14-ZkEV#y5C+{Pq87Gu3&>g zFy~Wk7^6v*)4pF3@F@rE__k3ikx(hzN3@e*^0=KNA6|jC^B5nf(XaoQaZN?Xi}Rn3 z$8&m*KmWvPaUQ(V<#J+S&zO|8P-#!f%7G+n_%sXp9=J%Z4&9OkWXeuZN}ssgQ#Tcj z8p6ErJQJWZ+fXLCco=RN8D{W%+*kko*2-LEb))xcHwNl~Xmir>kmAxW?eW50Osw3# zki8Fl$#fvw*7rqd?%E?}ZX4`c5-R&w!Y0#EBbelVXSng+kUfeUiqofPehl}$ormli zg%r)}?%=?_pHb9`Cq9Z|B`L8b>(!+8HSX?`5+5mm81AFXfnAt1*R3F z%b2RPIacKAddx%JfQ8l{3U|vK@W7KB$CdLqn@wP^?azRks@x8z59#$Q*7q!KilY-P zHUbs(IFYRGG1{~@RF;Lqyho$~7^hNC`NL3kn^Td%A7dRgr_&`2k=t+}D-o9&C!y^? z6MsQ=tc3g0xkK(O%DzR9nbNB(r@L;1zQrs8mzx&4dz}?3KNYozOW5;=w18U6$G4U2 z#2^qRLT*Mo4bV1Oeo1PKQ2WQS2Y-hv&S|C7`xh6=Pj7MNLC5K-zokZ67S)C;(F0Dd zloDK2_o1$Fmza>EMj3X9je7e%Q`$39Dk~GoOj89-6q9|_WJlSl!!+*{R=tGp z8u|MuSwm^t7K^nUe+^0G3dkGZr3@(X+TL5eah)K^Tn zXEtHmR9UIaEYgD5Nhh(s*fcG_lh-mfy5iUF3xxpRZ0q3nZ=1qAtUa?(LnT9I&~uxX z`pV?+=|-Gl(kz?w!zIieXT}o}7@`QO>;u$Z!QB${a08_bW0_o@&9cjJUXzVyNGCm8 zm=W+$H!;_Kzp6WQqxUI;JlPY&`V}9C$8HZ^m?NvI*JT@~BM=()T()Ii#+*$y@lTZBkmMMda>7s#O(1YZR+zTG@&}!EXFG{ zEWPSDI5bFi;NT>Yj*FjH((=oe%t%xYmE~AGaOc4#9K_XsVpl<4SP@E!TgC0qpe1oi zNpxU2b0(lEMcoibQ-G^cxO?ySVW26HoBNa;n0}CWL*{k)oBu1>F18X061$SP{Gu67 z-v-Fa=Fl^u3lnGY^o5v)Bux}bNZ~ z5pL+7F_Esoun8^5>z8NFoIdb$sNS&xT8_|`GTe8zSXQzs4r^g0kZjg(b0bJvz`g<70u9Z3fQILX1Lj@;@+##bP|FAOl)U^9U>0rx zGi)M1(Hce)LAvQO-pW!MN$;#ZMX?VE(22lTlJrk#pB0FJNqVwC+*%${Gt#r_tH9I_ z;+#)#8cWAl?d@R+O+}@1A^hAR1s3UcW{G+>;X4utD2d9X(jF555}!TVN-hByV6t+A zdFR^aE@GNNgSxxixS2p=on4(+*+f<8xrwAObC)D5)4!z7)}mTpb7&ofF3u&9&wPS< zB62WHLGMhmrmOAgmJ+|c>qEWTD#jd~lHNgT0?t-p{T=~#EMcB| z=AoDKOL+qXCfk~F)-Rv**V}}gWFl>liXOl7Uec_8v)(S#av99PX1sQIVZ9eNLkhq$ zt|qu0b?GW_uo}TbU8!jYn8iJeIP)r@;!Ze_7mj{AUV$GEz6bDSDO=D!&C9!M@*S2! zfGyA|EPlXGMjkH6x7OMF?gKL7{GvGfED=Jte^p=91FpCu)#{whAMw`vSLa`K#atdN zThnL+7!ZNmP{rc=Z>%$meH;Qi1=m1E3Lq2D_O1-X5C;!I0L>zur@tPAC9*7Jeh)`;eec}1`nkRP(%iv-`N zZ@ip-g|7l6Hz%j%gcAM}6-nrC8oA$BkOTz^?dakvX?`^=ZkYh%vUE z9+&)K1UTK=ahYiaNn&G5nHUY5niLGus@p5E2@RwZufRvF{@$hW{;{3QhjvEHMvduO z#Wf-@oYU4ht?#uP{N3utVzV49mEc9>*TV_W2TVC`6+oI)zAjy$KJrr=*q##&kobiQ z1vNbya&OVjK`2pdRrM?LuK6BgrLN7H_3m z!qpNKg~87XgCwb#I=Q&0rI*l$wM!qTkXrx1ko5q-f;=R2fImRMwt5Qs{P*p^z@9ex z`2#v(qE&F%MXlHpdO#QEZyZftn4f05ab^f2vjxuFaat2}jke{j?5GrF=WYBR?gS(^ z9SBiNi}anzBDBRc+QqizTTQuJrzm^bNA~A{j%ugXP7McZqJ}65l10({wk++$=e8O{ zxWjG!Qp#5OmI#XRQQM?n6?1ztl6^D40hDJr?4$Wc&O_{*OfMfxe)V0=e{|N?J#fgE>j9jAajze$iN!*yeF%jJU#G1c@@rm zolGW!j?W6Q8pP=lkctNFdfgUMg92wlM4E$aks1??M$~WQfzzzXtS)wKrr2sJeCN4X zY(X^H_c^PzfcO8Bq(Q*p4c_v@F$Y8cHLrH$`pJ2}=#*8%JYdqsqnGqEdBQMpl!Ot04tUGSXTQdsX&GDtjbWD=prcCT9(+ z&UM%lW%Q3yrl1yiYs;LxzIy>2G}EPY6|sBhL&X&RAQrSAV4Tlh2nITR?{6xO9ujGu zr*)^E`>o!c=gT*_@6S&>0POxcXYNQd&HMw6<|#{eSute2C3{&h?Ah|cw56-AP^f8l zT^kvZY$YiH8j)sk7_=;gx)vx-PW`hbSBXJGCTkpt;ap(}G2GY=2bbjABU5)ty%G#x zAi07{Bjhv}>OD#5zh#$0w;-vvC@^}F! z#X$@)zIs1L^E;2xDAwEjaXhTBw2<{&JkF*`;c3<1U@A4MaLPe{M5DGGkL}#{cHL%* zYMG+-Fm0#qzPL#V)TvQVI|?_M>=zVJr9>(6ib*#z8q@mYKXDP`k&A4A};xMK0h=yrMp~JW{L?mE~ph&1Y1a#4%SO)@{ zK2juwynUOC)U*hVlJU17%llUxAJFuKZh3K0gU`aP)pc~bE~mM!i1mi!~LTf>1Wp< zuG+ahp^gH8g8-M$u{HUWh0m^9Rg@cQ{&DAO{PTMudV6c?ka7+AO& z746QylZ&Oj`1aqfu?l&zGtJnpEQOt;OAFq19MXTcI~`ZcoZmyMrIKDFRIDi`FH)w; z8+*8tdevMDv*VtQi|e}CnB_JWs>fhLOH-+Os2Lh!&)Oh2utl{*AwR)QVLS49iTp{6 z;|172Jl!Ml17unF+pd+Ff@jIE-{Oxv)5|pOm@CkHW?{l}b@1>Pe!l}VccX#xp@xgJ zyE<&ep$=*vT=}7vtvif0B?9xw_3Gej7mN*dOHdQPtW5kA5_zGD zpA4tV2*0E^OUimSsV#?Tg#oiQ>%4D@1F5@AHwT8Kgen$bSMHD3sXCkq8^(uo7CWk`mT zuslYq`6Yz;L%wJh$3l1%SZv#QnG3=NZ=BK4yzk#HAPbqXa92;3K5?0kn4TQ`%E%X} z&>Lbt!!QclYKd6+J7Nl@xv!uD%)*bY-;p`y^ZCC<%LEHUi$l5biu!sT3TGGSTPA21 zT8@B&a0lJHVn1I$I3I1I{W9fJAYc+8 zVj8>HvD}&O`TqU2AAb={?eT;0hyL(R{|h23=4fDSZKC32;wWxsVj`P z3J3{M$PwdH!ro*Cn!D&=jnFR>BNGR<<|I8CI@+@658Dy(lhqbhXfPTVecY@L8%`3Q z1Fux2w?2C3th60jI~%OC9BtpNF$QPqcG+Pz96qZJ71_`0o0w_q7|h&O>`6U+^BA&5 zXd5Zp1Xkw~>M%RixTm&OqpNl8Q+ue=92Op_>T~_9UON?ZM2c0aGm=^A4ejrXj3dV9 zhh_bCt-b9`uOX#cFLj!vhZ#lS8Tc47OH>*)y#{O9?AT~KR9LntM|#l#Dlm^8{nZdk zjMl#>ZM%#^nK2TPzLcKxqx24P7R1FPlBy7LSBrRvx>fE$9AJ;7{PQm~^LBX^k#6Zq zw*Z(zJC|`!6_)EFR}8|n8&&Rbj8y028~P~sFXBFRt+tmqH-S3<%N;C&WGH!f3{7cm zy_fCAb9@HqaXa1Y5vFbxWf%#zg6SI$C+Uz5=CTO}e|2fjWkZ;Dx|84Ow~bkI=LW+U zuq;KSv9VMboRvs9)}2PAO|b(JCEC_A0wq{uEj|3x@}*=bOd zwr{TgeCGG>HT<@Zeq8y}vTpwDg#UBvD)BEs@1KP$^3$sh&_joQPn{hjBXmLPJ{tC) z*HS`*2+VtJO{|e$mM^|qv1R*8i(m1`%)}g=SU#T#0KlTM2RSvYUc1fP+va|4;5}Bfz98UvDCpq7}+SMV&;nX zQw~N6qOX{P55{#LQkrZk(e5YGzr|(B;Q;ju;2a`q+S9bsEH@i1{_Y0;hWYn1-79jl z5c&bytD*k)GqrVcHn6t-7kinadiD>B{Tl`ZY@`g|b~pvHh5!gKP4({rp?D0aFd_cN zhHRo4dd5^S6ViN(>(28qZT6E>??aRhc($kP`>@<+lIKS5HdhjVU;>f7<4))E*5|g{ z&d1}D|vpuV^eRj5j|xx9nwaCxXFG?Qbjn~_WSy=N}P0W>MP zG-F%70lX5Xr$a)2i6?i|iMyM|;Jtf*hO?=Jxj12oz&>P=1#h~lf%#fc73M2_(SUM- zf&qnjS80|_Y0lDgl&I?*eMumUklLe_=Td!9G@eR*tcPOgIShJipp3{A10u(4eT~DY zHezEj8V+7m!knn7)W!-5QI3=IvC^as5+TW1@Ern@yX| z7Nn~xVx&fGSr+L%4iohtS3w^{-H1A_5=r&x8}R!YZvp<2T^YFvj8G_vm}5q;^UOJf ztl=X3iL;;^^a#`t{Ae-%5Oq{?M#s6Npj+L(n-*LMI-yMR{)qki!~{5z{&`-iL}lgW zxo+tnvICK=lImjV$Z|O_cYj_PlEYCzu-XBz&XC-JVxUh9;6*z4fuBG+H{voCC;`~GYV|hj%j_&I zDZCj>Q_0RCwFauYoVMiUSB+*Mx`tg)bWmM^SwMA+?lBg12QUF_x2b)b?qb88K-YUd z0dO}3k#QirBV<5%jL$#wlf!60dizu;tsp(7XLdI=eQs?P`tOZYMjVq&jE)qK*6B^$ zBe>VvH5TO>s>izhwJJ$<`a8fakTL!yM^Zfr2hV9`f}}VVUXK39p@G|xYRz{fTI+Yq z20d=)iwjuG9RB$%$^&8#(c0_j0t_C~^|n+c`Apu|x7~;#cS-s=X1|C*YxX3ailhg_|0`g!E&GZJEr?bh#Tpb8siR=JxWKc{#w7g zWznLwi;zLFmM1g8V5-P#RsM@iX>TK$xsWuujcsVR^7TQ@!+vCD<>Bk9tdCo7Mzgq5 zv8d>dK9x8C@Qoh01u@3h0X_`SZluTb@5o;{4{{eF!-4405x8X7hewZWpz z2qEi4UTiXTvsa(0X7kQH{3VMF>W|6;6iTrrYD2fMggFA&-CBEfSqPlQDxqsa>{e2M z(R5PJ7uOooFc|9GU0ELA%m4&4Ja#cQpNw8i8ACAoK6?-px+oBl_yKmenZut#Xumjz zk8p^OV2KY&?5MUwGrBOo?ki`Sxo#?-Q4gw*Sh0k`@ zFTaYK2;}%Zk-68`#5DXU$2#=%YL#S&MTN8bF+!J2VT6x^XBci6O)Q#JfW{YMz) zOBM>t2rSj)n#0a3cjvu}r|k3od6W(SN}V-cL?bi*Iz-8uOcCcsX0L>ZXjLqk zZu2uHq5B|Kt>e+=pPKu=1P@1r9WLgYFq_TNV1p9pu0erHGd!+bBp!qGi+~4A(RsYN@CyXNrC&hxGmW)u5m35OmWwX`I+0yByglO`}HC4nGE^_HUs^&A(uaM zKPj^=qI{&ayOq#z=p&pnx@@k&I1JI>cttJcu@Ihljt?6p^6{|ds`0MoQwp+I{3l6` zB<9S((RpLG^>=Kic`1LnhpW2=Gu!x`m~=y;A`Qk!-w`IN;S8S930#vBVMv2vCKi}u z6<-VPrU0AnE&vzwV(CFC0gnZYcpa-l5T0ZS$P6(?9AM;`Aj~XDvt;Jua=jIgF=Fm? zdp=M$>`phx%+Gu};;-&7T|B1AcC#L4@mW5SV_^1BRbo6;2PWe$r+npRV`yc;T1mo& z+~_?7rA+(Um&o@Tddl zL_hxvWk~a)yY}%j`Y+200D%9$bWHy&;(yj{jpi?Rtz{J66ANw)UyPOm;t6FzY3$hx zcn)Ir79nhFvNa7^a{SHN7XH*|Vlsx`CddPnA&Qvh8aNhEA;mPVv;Ah=k<*u!Zq^7 z<=xs*iQTQOMMcg|(NA_auh@x`3#_LFt=)}%SQppP{E>mu_LgquAWvh<>L7tf9+~rO znwUDS52u)OtY<~!d$;m9+87aO+&`#2ICl@Y>&F{jI=H(K+@3M1$rr=*H^dye#~TyD z!){#Pyfn+|ugUu}G;a~!&&0aqQ59U@UT3|_JuBlYUpT$2+11;}JBJ`{+lQN9T@QFY z5+`t;6(TS0F?OlBTE!@7D`8#URDNqx2t6`GZ{ZgXeS@v%-eJzZOHz18aS|svxII$a zZeFjrJ*$IwX$f-Rzr_G>xbu@euGl)B7pC&S+CmDJBg$BoV~jxSO#>y z33`bupN#LDoW0feZe0%q8un0rYN|eRAnwDHQ6e_)xBTbtoZtTA=Fvk){q}9Os~6mQ zKB80VI_&6iSq`LnK7*kfHZoeX6?WE}8yjuDn=2#JG$+;-TOA1%^=DnXx%w{b=w}tS zQbU3XxtOI8E(!%`64r2`zog;5<0b4i)xBmGP^jiDZ2%HNSxIf3@wKs~uk4%3Mxz;~ zts_S~E4>W+YwI<-*-$U8*^HKDEa8oLbmqGg?3vewnaNg%Mm)W=)lcC_J+1ov^u*N3 zXJ?!BrH-+wGYziJq2Y#vyry6Z>NPgkEk+Ke`^DvNRdb>Q2Nlr#v%O@<5hbflI6EKE z9dWc0-ORk^T}jP!nkJ1imyjdVX@GrjOs%cpgA8-c&FH&$(4od#x6Y&=LiJZPINVyW z0snY$8JW@>tc2}DlrD3StQmA0Twck~@>8dSix9CyQOALcREdxoM$Sw*l!}bXKq9&r zysMWR@%OY24@e`?+#xV2bk{T^C_xSo8v2ZI=lBI*l{RciPwuE>L5@uhz@{!l)rtVlWC>)6(G)1~n=Q|S!{E9~6*fdpa*n z!()-8EpTdj=zr_Lswi;#{TxbtH$8*G=UM`I+icz7sr_SdnHXrv=?iEOF1UL+*6O;% zPw>t^kbW9X@oEXx<97%lBm-9?O_7L!DeD)Me#rwE54t~UBu9VZ zl_I1tBB~>jm@bw0Aljz8! zXBB6ATG6iByKIxs!qr%pz%wgqbg(l{65DP4#v(vqhhL{0b#0C8mq`bnqZ1OwFV z7mlZZJFMACm>h9v^2J9+^_zc1=JjL#qM5ZHaThH&n zXPTsR8(+)cj&>Un{6v*z?@VTLr{TmZ@-fY%*o2G}*G}#!bmqpoo*Ay@U!JI^Q@7gj;Kg-HIrLj4}#ec4~D2~X6vo;ghep-@&yOivYP zC19L0D`jjKy1Yi-SGPAn94(768Tcf$urAf{)1)9W58P`6MA{YG%O?|07!g9(b`8PXG1B1Sh0?HQmeJtP0M$O$hI z{5G`&9XzYhh|y@qsF1GnHN|~^ru~HVf#)lOTSrv=S@DyR$UKQk zjdEPFDz{uHM&UM;=mG!xKvp;xAGHOBo~>_=WFTmh$chpC7c`~7?36h)7$fF~Ii}8q zF|YXxH-Z?d+Q+27Rs3X9S&K3N+)OBxMHn1u(vlrUC6ckBY@@jl+mgr#KQUKo#VeFm zFwNYgv0<%~Wn}KeLeD9e1$S>jhOq&(e*I@L<=I5b(?G(zpqI*WBqf|Zge0&aoDUsC zngMRA_Kt0>La+Erl=Uv_J^p(z=!?XHpenzn$%EA`JIq#yYF?JLDMYiPfM(&Csr#f{ zdd+LJL1by?xz|D8+(fgzRs~(N1k9DSyK@LJygwaYX8dZl0W!I&c^K?7)z{2is;OkE zd$VK-(uH#AUaZrp=1z;O*n=b?QJkxu`Xsw&7yrX0?(CX=I-C#T;yi8a<{E~?vr3W> zQrpPqOW2M+AnZ&p{hqmHZU-;Q(7?- zP8L|Q0RM~sB0w1w53f&Kd*y}ofx@c z5Y6B8qGel+uT1JMot$nT1!Tim6{>oZzJXdyA+4euOLME?5Fd_85Uk%#E*ln%y{u8Q z$|?|R@Hpb~yTVK-Yr_S#%NUy7EBfYGAg>b({J|5b+j-PBpPy$Ns`PaJin4JdRfOaS zE|<HjH%NuJgsd2wOlv>~y=np%=2)$M9LS|>P)zJ+Fei5vYo_N~B0XCn+GM76 z)Xz3tg*FRVFgIl9zpESgdpWAavvVViGlU8|UFY{{gVJskg*I!ZjWyk~OW-Td4(mZ6 zB&SQreAAMqwp}rjy`HsG({l2&q5Y52<@AULVAu~rWI$UbFuZs>Sc*x+XI<+ez%$U)|a^unjpiW0l0 zj1!K0(b6$8LOjzRqQ~K&dfbMIE=TF}XFAi)$+h}5SD3lo z%%Qd>p9se=VtQG{kQ;N`sI)G^u|DN#7{aoEd zkksYP%_X$Rq08);-s6o>CGJ<}v`qs%eYf+J%DQ^2k68C%nvikRsN?$ap--f+vCS`K z#&~)f7!N^;sdUXu54gl3L=LN>FB^tuK=y2e#|hWiWUls__n@L|>xH{%8lIJTd5`w? zSwZbnS;W~DawT4OwSJVdAylbY+u5S+ZH{4hAi2&}Iv~W(UvHg(1GTZRPz`@{SOqzy z(8g&Dz=$PfRV=6FgxN~zo+G8OoPI&d-thcGVR*_^(R8COTM@bq?fDwY{}WhsQS1AK zF6R1t8!RdFmfocpJ6?9Yv~;WYi~XPgs(|>{5})j!AR!voO7y9&cMPo#80A(`za@t>cx<0;qxM@S*m(jYP)dMXr*?q0E`oL;12}VAep179uEr8c<=D zr5?A*C{eJ`z9Ee;E$8)MECqatHkbHH z&Y+ho0B$31MIB-xm&;xyaFCtg<{m~M-QDbY)fQ>Q*Xibb~8ytxZQ?QMf9!%cV zU0_X1@b4d+Pg#R!`OJ~DOrQz3@cpiGy~XSKjZQQ|^4J1puvwKeScrH8o{bscBsowomu z^f12kTvje`yEI3eEXDHJ6L+O{Jv$HVj%IKb|J{IvD*l6IG8WUgDJ*UGz z3!C%>?=dlfSJ>4U88)V+`U-!9r^@AxJBx8R;)J4Fn@`~k>8>v0M9xp90OJElWP&R5 zM#v*vtT}*Gm1^)Bv!s72T3PB0yVIjJW)H7a)ilkAvoaH?)jjb`MP>2z{%Y?}83 zUIwBKn`-MSg)=?R)1Q0z3b>dHE^)D8LFs}6ASG1|daDly_^lOSy&zIIhm*HXm1?VS=_iacG);_I9c zUQH1>i#*?oPIwBMJkzi_*>HoUe}_4o>2(SHWzqQ=;TyhAHS;Enr7!#8;sdlty&(>d zl%5cjri8`2X^Ds`jnw7>A`X|bl=U8n+3LKLy(1dAu8`g@9=5iw$R0qk)w8Vh_Dt^U zIglK}sn^)W7aB(Q>HvrX=rxB z+*L)3DiqpQ_%~|m=44LcD4-bxO3OO*LPjsh%p(k?&jvLp0py57oMH|*IMa(<|{m1(0S|x)?R-mqJ=I;_YUZA>J z62v*eSK;5w!h8J+6Z2~oyGdZ68waWfy09?4fU&m7%u~zi?YPHPgK6LDwphgaYu%0j zurtw)AYOpYKgHBrkX189mlJ`q)w-f|6>IER{5Lk97%P~a-JyCRFjejW@L>n4vt6#hq;!|m;hNE||LK3nw1{bJOy+eBJjK=QqNjI;Q6;Rp5 z&035pZDUZ#%Oa;&_7x0T<7!RW`#YBOj}F380Bq?MjjEhrvlCATPdkCTTl+2efTX$k zH&0zR1n^`C3ef~^sXzJK-)52(T}uTG%OF8yDhT76L~|^+hZ2hiSM*QA9*D5odI1>& z9kV9jC~twA5MwyOx(lsGD_ggYmztXPD`2=_V|ks_FOx!_J8!zM zTzh^cc+=VNZ&(OdN=y4Juw)@8-85lwf_#VMN!Ed(eQiRiLB2^2e`4dp286h@v@`O%_b)Y~A; zv}r6U?zs&@uD_+(_4bwoy7*uozNvp?bXFoB8?l8yG0qsm1JYzIvB_OH4_2G*IIOwT zVl%HX1562vLVcxM_RG*~w_`FbIc!(T=3>r528#%mwwMK}uEhJ()3MEby zQQjzqjWkwfI~;Fuj(Lj=Ug0y`>~C7`w&wzjK(rPw+Hpd~EvQ-ufQOiB4OMpyUKJhw zqEt~jle9d7S~LI~$6Z->J~QJ{Vdn3!c}g9}*KG^Kzr^(7VI5Gk(mHLL{itj_hG?&K4Ws0+T4gLfi3eu$N=`s36geNC?c zm!~}vG6lx9Uf^5M;bWntF<-{p^bruy~f?sk9 zcETAPQZLoJ8JzMMg<-=ju4keY@SY%Wo?u9Gx=j&dfa6LIAB|IrbORLV1-H==Z1zCM zeZcOYpm5>U2fU7V*h;%n`8 zN95QhfD994={1*<2vKLCNF)feKOGk`R#K~G=;rfq}|)s20&MCa65 zUM?xF5!&e0lF%|U!#rD@I{~OsS_?=;s_MQ_b_s=PuWdC)q|UQ&ea)DMRh5>fpQjXe z%9#*x=7{iRCtBKT#H>#v%>77|{4_slZ)XCY{s3j_r{tdpvb#|r|sbS^dU1x70$eJMU!h{Y7Kd{dl}9&vxQl6Jt1a` zHQZrWyY0?!vqf@u-fxU_@+}u(%Wm>0I#KP48tiAPYY!TdW(o|KtVI|EUB9V`CBBNaBLVih7+yMVF|GSoIQD0Jfb{ z!OXq;(>Z?O`1gap(L~bUcp>Lc@Jl-})^=6P%<~~9ywY=$iu8pJ0m*hOPzr~q`23eX zgbs;VOxxENe0UMVeN*>uCn9Gk!4siN-e>x)pIKAbQz!G)TcqIJ0`JBBaX>1-4_XO_-HCS^vr2vjv#7KltDZdyQ{tlWh4$Gm zB>|O1cBDC)yG(sbnc*@w6e%e}r*|IhpXckx&;sQCwGdKH+3oSG-2)Bf#x`@<4ETAr z0My%7RFh6ZLiZ_;X6Mu1YmXx7C$lSZ^}1h;j`EZd6@%JNUe=btBE z%s=Xmo1Ps?8G`}9+6>iaB8bgjUdXT?=trMu|4yLX^m0Dg{m7rpKNJey|EwHI+nN1e zL^>qN%5Fg)dGs4DO~uwIdXImN)QJ*Jhpj7$fq_^`{3fwpztL@WBB}OwQ#Epo-mqMO zsM$UgpFiG&d#)lzEQ{3Q;)&zTw;SzGOah-Dpm{!q7<8*)Ti_;xvV2TYXa}=faXZy? z3y?~GY@kl)>G&EvEijk9y1S`*=zBJSB1iet>0;x1Ai)*`^{pj0JMs)KAM=@UyOGtO z3y0BouW$N&TnwU6!%zS%nIrnANvZF&vB1~P5_d`x-giHuG zPJ;>XkVoghm#kZXRf>qxxEix;2;D1CC~NrbO6NBX!`&_$iXwP~P*c($EVV|669kDO zKoTLZNF4Cskh!Jz5ga9uZ`3o%7Pv`d^;a=cXI|>y;zC3rYPFLQkF*nv(r>SQvD*## z(Vo%^9g`%XwS0t#94zPq;mYGLKu4LU3;txF26?V~A0xZbU4Lmy`)>SoQX^m7fd^*E z+%{R4eN!rIk~K)M&UEzxp9dbY;_I^c} zOc{wlIrN_P(PPqi51k_$>Lt|X6A^|CGYgKAmoI#Li?;Wq%q~q*L7ehZkUrMxW67Jl zhsb~+U?33QS>eqyN{(odAkbopo=Q$Az?L+NZW>j;#~@wCDX?=L5SI|OxI~7!Pli;e zELMFcZtJY3!|=Gr2L4>z8yQ-{To>(f80*#;6`4IAiqUw`=Pg$%C?#1 z_g@hIGerILSU>=P>z{gM|DS91A4cT@PEIB^hSop!uhMo#2G;+tQSpDO_6nOnPWSLU zS;a9m^DFMXR4?*X=}d7l;nXuHk&0|m`NQn%d?8|Ab3A9l9Jh5s120ibWBdB z$5YwsK3;wvp!Kn@)Qae{ef`0#NwlRpQ}k^r>yos_Ne1;xyKLO?4)t_G4eK~wkUS2A&@_;)K0-03XGBzU+5f+uMDxC z(s8!8!RvdC#@`~fx$r)TKdLD6fWEVdEYtV#{ncT-ZMX~eI#UeQ-+H(Z43vVn%Yj9X zLdu9>o%wnWdvzA-#d6Z~vzj-}V3FQ5;axDIZ;i(95IIU=GQ4WuU{tl-{gk!5{l4_d zvvb&uE{%!iFwpymz{wh?bKr1*qzeZb5f6e6m_ozRF&zux2mlK=v_(_s^R6b5lu?_W4W3#<$zeG~Pd)^!4tzhs}-Sx$FJP>)ZGF(hVTH|C3(U zs0PO&*h_ zNA-&qZpTP$$LtIgfiCn07}XDbK#HIXdmv8zdz4TY;ifNIH-0jy(gMSByG2EF~Th#eb_TueZC` zE?3I>UTMpKQ})=C;6p!?G)M6w^u*A57bD?2X`m3X^6;&4%i_m(uGJ3Z5h`nwxM<)H z$I5m?wN>O~8`BGnZ=y^p6;0+%_0K}Dcg|K;+fEi|qoBqvHj(M&aHGqNF48~XqhtU? z^ogwBzRlOfpAJ+Rw7IED8lRbTdBdyEK$gPUpUG}j-M42xDj_&qEAQEtbs>D#dRd7Y z<&TpSZ(quQDHiCFn&0xsrz~4`4tz!CdL8m~HxZM_agu@IrBpyeL1Ft}V$HX_ZqDPm z-f89)pjuEzGdq-PRu`b1m+qBGY{zr_>{6Ss>F|xHZlJj9dt5HD$u`1*WZe)qEIuDSR)%z+|n zatVlhQ?$w#XRS7xUrFE;Y8vMGhQS5*T{ZnY=q1P?w5g$OKJ#M&e??tAmPWHMj3xhS ziGxapy?kn@$~2%ZY;M8Bc@%$pkl%Rvj!?o%agBvpQ-Q61n9kznC4ttrRNQ4%GFR5u zyv%Yo9~yxQJWJSfj z?#HY$y=O~F|2pZs22pu|_&Ajd+D(Mt!nPUG{|1nlvP`=R#kKH zO*s$r_%ss5h1YO7k0bHJ2CXN)Yd6CHn~W!R=SqkWe=&nAZu(Q1G!xgcUilM@YVei@2@a`8he z9@pM`)VB*=e7-MWgLlXlc)t;fF&-AwM{E-EX}pViFn0I0CNw2bNEnN2dj!^4(^zS3 zobUm1uQnpqk_4q{pl*n06=TfK_C>UgurKFjRXsK_LEn};=79`TB12tv6KzwSu*-C8 z;=~ohDLZylHQ|Mpx-?yql>|e=vI1Z!epyUpAcDCp4T|*RV&X`Q$0ogNwy6mFALo^@ z9=&(9txO8V@E!@6^(W0{*~CT>+-MA~vnJULBxCTUW>X5>r7*eXYUT0B6+w@lzw%n> z_VjJ<2qf|(d6jYq2(x$(ZDf!yVkfnbvNmb5c|hhZ^2TV_LBz`9w!e_V*W_(MiA7|= z&EeIIkw*+$Xd!)j8<@_<}A5;~A_>3JT*kX^@}cDoLd>Qj<`Se^wdUa(j0dp+Tl8EptwBm{9OGsdFEq zM`!pjf(Lm(`$e3FLOjqA5LnN5o!}z{ zNf}rJuZh@yUtq&ErjHeGzX4(!luV!jB&;FAP|!R_QHYw#^Z1LwTePAKJ6X&IDNO#; z)#I@Xnnzyij~C@UH~X51JCgQeF0&hTXnuoElz#m{heZRexWc0k4<>0+ClX7%0 zEBqCCld1tD9Zwkr4{?Nor19#E5-YKfB8d?qgR82-Ow2^AuNevly2*tHA|sK!ybYkX zm-sLQH72P&{vEAW6+z~O5d0qd=xW~rua~5a?ymYFSD@8&gV)E5@RNNBAj^C99+Z5Z zR@Pq55mbCQbz+Mn$d_CMW<-+?TU960agEk1J<>d>0K=pF19yN))a~4>m^G&tc*xR+yMD*S=yip-q=H zIlredHpsJV8H(32@Zxc@bX6a21dUV95Th--8pE6C&3F>pk=yv$yd6@Haw;$v4+Fcb zRwn{Qo@0`7aPa2LQOP}j9v>sjOo5Kqvn|`FLizX zB+@-u4Lw|jsvz{p^>n8Vo8H2peIqJJnMN}A)q6%$Tmig7eu^}K2 zrh$X?T|ZMsoh{6pdw1G$_T<`Ds-G=jc;qcGdK4{?dN2-XxjDNbb(7pk|3JUVCU4y; z)?LXR>f+AAu)JEiti_Zy#z5{RgsC}R(@jl%9YZ>zu~hKQ*AxbvhC378-I@{~#%Y`Z zy=a=9YpewPIC+gkEUUwtUL7|RU7=!^Aa}Mk^6uxOgRGA#JXjWLsjFUnix|Mau{hDT z7mn*z1m5g`vP(#tjT0Zy4eAY(br&!RiiXE=ZI!{sE1#^#%x^Z7t1U)b<;%Y}Q9=5v z;wpDCEZ@OE36TWT=|gxigT@VaW9BvHS05;_P(#s z8zI4XFQys}q)<`tkX$WnSarn{3e!s}4(J!=Yf>+Y>cP3f;vr63f2{|S^`_pWc)^5_!R z*(x-fuBxL51@xe!lnDBKi}Br$c$BMZ3%f2Sa6kLabiBS{pq*yj;q|k(86x`PiC{p6 z_bxCW{>Q2BA8~Ggz&0jkrcU+-$ANBsOop*ms>34K9lNYil@}jC;?cYP(m^P}nR6FV zk(M%48Z&%2Rx$A&FhOEirEhY0(dn;-k(qkTU)sFQ`+-ih+s@A8g?r8Pw+}2;35WYf zi}VO`jS`p(tc)$X$a>-#WXoW!phhatC*$}|rk>|wUU71eUJG^$c6_jwX?iSHM@6__ zvV|6%U*$sSXJu9SX?2%M^kK|}a2QJ8AhF{fuXrHZxXsI~O zGKX45!K7p*MCPEQ=gp?eu&#AW*pR{lhQR##P_*{c_DjMGL|3T3-bSJ(o$|M{ytU}> zAV>wq*uE*qFo9KvnA^@juy{x<-u*#2NvkV={Ly}ysKYB-k`K3@K#^S1Bb$8Y#0L0# z`6IkSG&|Z$ODy|VLS+y5pFJx&8tvPmMd8c9FhCyiU8~k6FwkakUd^(_ml8`rnl>JS zZV){9G*)xBqPz^LDqRwyS6w86#D^~xP4($150M)SOZRe9sn=>V#aG0Iy(_^YcPpIz8QYM-#s+n% z@Jd?xQq?Xk6=<3xSY7XYP$$yd&Spu{A#uafiIfy8gRC`o0nk{ezEDjb=q_qRAlR1d zFq^*9Gn)yTG4b}R{!+3hWQ+u3GT~8nwl2S1lpw`s0X_qpxv)g+JIkVKl${sYf_nV~B>Em>M;RlqGb5WVil(89 zs=ld@|#;dq1*vQGz=7--Br-|l) zZ%Xh@v8>B7P?~}?Cg$q9_={59l%m~O&*a6TKsCMAzG&vD>k2WDzJ6!tc!V)+oxF;h zJH;apM=wO?r_+*#;ulohuP=E>^zon}a$NnlcQ{1$SO*i=jnGVcQa^>QOILc)e6;eNTI>os=eaJ{*^DE+~jc zS}TYeOykDmJ=6O%>m`i*>&pO_S;qMySJIyP=}4E&J%#1zju$RpVAkZbEl+p%?ZP^C z*$$2b4t%a(e+%>a>d_f_<JjxI#J1x;=hPd1zFPx=6T$;;X1TD*2(edZ3f46zaAoW>L53vS_J*N8TMB|n+;LD| zC=GkQPpyDY#Am4l49chDv*gojhRj_?63&&8#doW`INATAo(qY#{q}%nf@eTIXmtU< zdB<7YWfyCmBs|c)cK>1)v&M#!yNj#4d$~pVfDWQc_ke1?fw{T1Nce_b`v|Vp5ig(H zJvRD^+ps46^hLX;=e2!2e;w9y1D@!D$c@Jc&%%%IL=+xzw55&2?darw=9g~>P z9>?Kdc$r?6c$m%x2S$sdpPl>GQZ{rC9mPS63*qjCVa?OIBj!fW zm|g?>CVfGXNjOfcyqImXR_(tXS(F{FcoNzKvG5R$IgGaxC@)i(e+$ME}vPVIhd|mx2IIE+f zM?9opQHIVgBWu)^A|RzXw!^??S!x)SZOwZaJkGjc<_}2l^eSBm!eAJG9T>EC6I_sy z?bxzDIAn&K5*mX)$RQzDA?s)-no-XF(g*yl4%+GBf`##bDXJ==AQk*xmnatI;SsLp zP9XTHq5mmS=iWu~9ES>b%Q=1aMa|ya^vj$@qz9S!ih{T8_PD%Sf_QrNKwgrXw9ldm zHRVR98*{C?_XNpJn{abA!oix_mowRMu^2lV-LPi;0+?-F(>^5#OHX-fPED zCu^l7u3E%STI}c4{J2!)9SUlGP_@!d?5W^QJXOI-Ea`hFMKjR7TluLvzC-ozCPn1`Tpy z!vlv@_Z58ILX6>nDjTp-1LlFMx~-%GA`aJvG$?8*Ihn;mH37eK**rmOEwqegf-Ccx zrIX4;{c~RK>XuTXxYo5kMiWMy)!IC{*DHG@E$hx?RwP@+wuad(P1{@%tRkyJRqD)3 zMHHHZ4boqDn>-=DgR5VlhQTpfVy182Gk;A_S8A1-;U1RR>+$62>(MUx@Nox$vTjHq z%QR=j!6Gdyb5wu7y(YUktwMuW5<@jl?m4cv4BODiT5o8qVdC0MBqGr@-YBIwnpZAY znX9(_uQjP}JJ=!~Ve9#5I~rUnN|P_3D$LqZcvBnywYhjlMSFHm`;u9GPla{5QD7(7*6Tb3Svr8;(nuAd81q$*uq6HC_&~je*Ca7hP4sJp0av{M8480wF zxASi7Qv+~@2U%Nu1Ud;s-G4CTVWIPyx!sg&8ZG0Wq zG_}i3C(6_1>q3w!EH7$Kwq8uBp2F2N7}l65mk1p*9v0&+;th=_E-W)E;w}P(j⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ac72c34e8a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..0adc8e1a53 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# 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 + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..20365b4d8f --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'springboot-basic' diff --git a/voucher.csv b/voucher.csv new file mode 100644 index 0000000000..26fd26a0bb --- /dev/null +++ b/voucher.csv @@ -0,0 +1,2 @@ +"BENEFITVALUE","ID","VOUCHERTYPE" +"12","d1922fff-145d-4ea5-8ad9-a823571bca19","FIXED" From 4a223cf95f74c4d2d6f9815bc4f031ca34f8fdc9 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:22:40 +0900 Subject: [PATCH 02/97] =?UTF-8?q?[feat]=20csv=20=ED=8C=8C=EC=9D=BC=20util?= =?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 --- .../exceptionClass/SystemException.java | 10 +++ .../springbootbasic/util/CsvManager.java | 81 +++++++++++++++++++ .../springbootbasic/util/FileManager.java | 11 +++ 3 files changed, 102 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java create mode 100644 src/main/java/com/programmers/springbootbasic/util/CsvManager.java create mode 100644 src/main/java/com/programmers/springbootbasic/util/FileManager.java diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java new file mode 100644 index 0000000000..f186d2b9d8 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class SystemException extends RuntimeException{ + + public SystemException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java new file mode 100644 index 0000000000..9ce9cdb7ff --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java @@ -0,0 +1,81 @@ +package com.programmers.springbootbasic.util; + + +import static com.programmers.springbootbasic.exception.ErrorCode.FILE_IO_ERROR; + +import com.opencsv.bean.CsvToBean; +import com.opencsv.bean.CsvToBeanBuilder; +import com.opencsv.bean.StatefulBeanToCsv; +import com.opencsv.bean.StatefulBeanToCsvBuilder; +import com.opencsv.exceptions.CsvDataTypeMismatchException; +import com.opencsv.exceptions.CsvRequiredFieldEmptyException; +import com.programmers.springbootbasic.exception.exceptionClass.SystemException; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.springframework.core.io.PathResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +@Component +public class CsvManager implements FileManager{ + + private static final Set EXTENSION = Set.of("csv"); + + @Override + public boolean supports(String fileName) { + String extension = fileName.substring(fileName.lastIndexOf(".") + 1); + return EXTENSION.contains(extension); + } + + @Override + public List read(String fileName, Class type){ + Resource resource = new PathResource(fileName); + + if (!resource.exists()) { + return new ArrayList<>(); // 파일이 없으면 빈 리스트 반환 + } + + try (Reader reader = new FileReader(resource.getFile())) { + // CsvToBean 객체 생성 및 파싱 + CsvToBean csvToBean = new CsvToBeanBuilder(reader) + .withType(type) + .withIgnoreLeadingWhiteSpace(true) + .build(); + + return csvToBean.parse(); + } catch (IOException e) { + System.out.println(e.getMessage()); + throw new SystemException(FILE_IO_ERROR); + } + } + + @Override + public void write(T entity, String fileName) { + try { + Resource resource = new PathResource(fileName); + File file = null; + if (!resource.exists()) { + resource.getFile().createNewFile(); + } + file = resource.getFile(); + try (FileWriter writer = new FileWriter(file)) { + StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer) + .withSeparator(',') + .build(); + + beanToCsv.write(entity); + } + } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) { + throw new SystemException(FILE_IO_ERROR); + } + } + + + +} diff --git a/src/main/java/com/programmers/springbootbasic/util/FileManager.java b/src/main/java/com/programmers/springbootbasic/util/FileManager.java new file mode 100644 index 0000000000..2a02126120 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/util/FileManager.java @@ -0,0 +1,11 @@ +package com.programmers.springbootbasic.util; + +import java.util.List; + +public interface FileManager { + boolean supports(String fileName); + + List read(String fileName, Class type); + + void write(T entity, String fileName); +} From 6b464fefaf8b37d225d6837dabb567dbc878879e Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:26:59 +0900 Subject: [PATCH 03/97] =?UTF-8?q?[feat]=20=EC=BD=98=EC=86=94=20=EC=9E=85?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8826194a8fdada5b705be8ef8cb5dc127a515ce6. --- .../infrastructure/IO/Console.java | 74 +++++++++++++++++++ .../IO/ConsoleInteractionAggregator.java | 28 +++++++ .../presentation/IOManager.java | 6 ++ 3 files changed, 108 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java create mode 100644 src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java create mode 100644 src/main/java/com/programmers/springbootbasic/presentation/IOManager.java diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java new file mode 100644 index 0000000000..15ca429a5d --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java @@ -0,0 +1,74 @@ +package com.programmers.springbootbasic.infrastructure.IO; + +import com.programmers.springbootbasic.presentation.IOManager; +import com.programmers.springbootbasic.presentation.validation.InputValidator; +import jakarta.annotation.PostConstruct; +import java.util.Scanner; +import org.springframework.stereotype.Component; + +@Component +public class Console implements IOManager { + private final Scanner scanner = new Scanner(System.in); + private InputValidator stringValidator; + private InputValidator integerValidator; + + @PostConstruct + private void setValidator() { + InputValidator stringValidator = new InputValidator<>(); + stringValidator.addValidator(input -> { + if(input == null || input.isEmpty()) { + return "입력 값이 없습니다."; + } + return null; + }); + this.stringValidator = stringValidator; + + InputValidator integerValidator = new InputValidator<>(); + integerValidator.addValidator(input -> { + if(input == null || input.isEmpty()) { + return "입력 값이 없습니다."; + } + return null; + }); + integerValidator.addValidator(input -> { + try { + Integer.parseInt(input); + return null; + } catch (NumberFormatException e) { + return "숫자가 아닙니다."; + } + }); + this.integerValidator = integerValidator; + } + + public String collectStringInput(String message) { + while(true) { + print(message); + var result = scanner.nextLine().trim(); + var errors = stringValidator.validate(result); + + if(errors.isEmpty()) { + return result; + } + errors.get().forEach(this::print); + } + } + + public int collectIntegerInput(String message) { + while(true) { + print(message); + var result = scanner.nextLine().trim(); + var errors = integerValidator.validate(result); + + if(errors.isEmpty()) { + return Integer.parseInt(result); + } + errors.get().forEach(this::print); + } + } + + public void print(String message) { + System.out.println(message); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java new file mode 100644 index 0000000000..b41dc74e65 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.infrastructure.IO; + +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.util.Messages; +import org.springframework.stereotype.Component; + +@Component +public class ConsoleInteractionAggregator{ + private final Console console; + + public ConsoleInteractionAggregator(Console console) { + this.console = console; + } + + public String collectMenuInput() { + return console.collectStringInput(Messages.SELECT_MENU.getMessage()); + } + + public CreateVoucherRequest collectVoucherInput() { + String title = console.collectStringInput(Messages.VOUCHER_REGISTER_TYPE.getMessage()); + int amount = console.collectIntegerInput(Messages.VOUCHER_REGISTER_AMOUNT.getMessage()); + return CreateVoucherRequest.from(title, amount); + } + + public void displayMessage(String message) { + console.print(message); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java b/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java new file mode 100644 index 0000000000..b584f43718 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java @@ -0,0 +1,6 @@ +package com.programmers.springbootbasic.presentation; + +public interface IOManager { + String collectStringInput(String message); + void print(String message); +} From a58e23f68fe05e265f40275733916a06df99890e Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:27:26 +0900 Subject: [PATCH 04/97] =?UTF-8?q?[feat]=20=EC=9E=85=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EA=B8=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validation/InputValidator.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java diff --git a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java new file mode 100644 index 0000000000..c86cbfb470 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java @@ -0,0 +1,38 @@ +package com.programmers.springbootbasic.presentation.validation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class InputValidator { + + protected final List> validators = new ArrayList<>(); + + @FunctionalInterface + public interface validator { + String getErrorMessage(T val); + } + + public Optional> validate(T input) { + var errs = getError(input); + if(errs.isEmpty()) { + return Optional.empty(); + } + return Optional.of(errs); + } + + public void addValidator(validator validator) { + this.validators.add(validator); + } + + private List getError(T input) { + List errorMessages = new ArrayList<>(); + for (validator validator : validators) { + String errorMessage = validator.getErrorMessage(input); + if (errorMessage != null) { + errorMessages.add(errorMessage); + } + } + return errorMessages; + } +} From e243c9e7fc79163ec618267a3c0daaf163a57ea6 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:29:14 +0900 Subject: [PATCH 05/97] =?UTF-8?q?[feat]=20=EC=BD=98=EC=86=94=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=9C=BC=EB=A1=9C=20=EB=A9=94=EB=89=B4=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mediator/ConsoleRequest.java | 25 ++++++++ .../mediator/ConsoleResponse.java | 25 ++++++++ .../mediator/RequestProcessor.java | 62 +++++++++++++++++++ .../presentation/ControllerAdapter.java | 40 ++++++++++++ .../presentation/MainMenu.java | 47 ++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java create mode 100644 src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java create mode 100644 src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java diff --git a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java new file mode 100644 index 0000000000..4d086cc1e6 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java @@ -0,0 +1,25 @@ +package com.programmers.springbootbasic.mediator; + +import java.util.Optional; + +public class ConsoleRequest { + private String command; + private T body; + + public ConsoleRequest(String command) { + this.command = command; + } + + public ConsoleRequest(String command,T body) { + this.command = command; + this.body = body; + } + + public Optional getBody() { + return Optional.ofNullable(body); + } + + public String getCommand() { + return command; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java new file mode 100644 index 0000000000..61fecb0116 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java @@ -0,0 +1,25 @@ +package com.programmers.springbootbasic.mediator; + +import java.util.Optional; + +public class ConsoleResponse { + private T body; + private String message; + + public ConsoleResponse(T body, String message) { + this.body = body; + this.message = message; + } + + public ConsoleResponse(String message) { + this.message = message; + } + + public Optional getMessage() { + return Optional.ofNullable(this.message); + } + + public Optional getBody() { + return Optional.ofNullable(this.body); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java new file mode 100644 index 0000000000..fc00832091 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java @@ -0,0 +1,62 @@ +package com.programmers.springbootbasic.mediator; + +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.provider.MenuRequestProvider; +import com.programmers.springbootbasic.presentation.ControllerAdapter; +import com.programmers.springbootbasic.presentation.MainMenu; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class RequestProcessor { + + private final ControllerAdapter controllerAdapter; + private final MenuRequestProvider menuRequestProvider; + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public RequestProcessor(ControllerAdapter controllerAdapter, + MenuRequestProvider menuRequestProvider, + ConsoleInteractionAggregator consoleInteractionAggregator) { + this.controllerAdapter = controllerAdapter; + this.menuRequestProvider = menuRequestProvider; + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + public void run() { + ConsoleRequest request = getRequest(); + ConsoleResponse response = process(request); + sendResponse(response); + } + + public ConsoleRequest getRequest() { + var menuInput = consoleInteractionAggregator.collectMenuInput(); + // 종료 처리는 어디서? + if (menuInput.equalsIgnoreCase("EXIT")) { + System.exit(0); + } + return menuRequestProvider.getMenuRequest(menuInput); + } + + public ConsoleResponse process(ConsoleRequest request) { + return MainMenu.routeToController(request, controllerAdapter); + } + + public void sendResponse(ConsoleResponse response) { + response.getBody().ifPresent((body) -> { + if (body instanceof List) { + List listBody = (List) body; + listBody.forEach( + item -> consoleInteractionAggregator.displayMessage(item.toString())); + } else { + consoleInteractionAggregator.displayMessage(body.toString()); + } + }); + + response.getMessage().ifPresent( + (message) -> consoleInteractionAggregator.displayMessage( + (String) message) + ); + + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java new file mode 100644 index 0000000000..de16903b79 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -0,0 +1,40 @@ +package com.programmers.springbootbasic.presentation; + +import static com.programmers.springbootbasic.util.Messages.SUCCESS_BLACK_USER_LIST; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_LIST; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_REGISTER; + +import com.programmers.springbootbasic.domain.user.presentation.UserController; +import com.programmers.springbootbasic.domain.voucher.presentation.VoucherController; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.mediator.ConsoleResponse; +import org.springframework.stereotype.Component; + +@Component +public class ControllerAdapter { + + private final VoucherController voucherController; + private final UserController userController; + + public ControllerAdapter(VoucherController voucherController, UserController userController) { + this.voucherController = voucherController; + this.userController = userController; + } + + public ConsoleResponse createVoucher(Object... params) { + CreateVoucherRequest request = (CreateVoucherRequest) params[0]; + voucherController.createVoucher(request); + return new ConsoleResponse<>(SUCCESS_VOUCHER_REGISTER.getMessage()); + } + + public ConsoleResponse getAllVouchers(Object... params) { + return new ConsoleResponse(voucherController.getAllVouchers(), + SUCCESS_VOUCHER_LIST.getMessage() + ); + } + + public ConsoleResponse getBlackList(Object... params) { + return new ConsoleResponse(userController.getBlackList(), + SUCCESS_BLACK_USER_LIST.getMessage()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java new file mode 100644 index 0000000000..421559f88d --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -0,0 +1,47 @@ +package com.programmers.springbootbasic.presentation; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_MENU; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.mediator.ConsoleResponse; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +public enum MainMenu { + CREATE_VOUCHER("create voucher",ControllerAdapter::createVoucher), + LIST_VOUCHER("list voucher",ControllerAdapter::getAllVouchers), + LIST_BLACK_USER("list black user",ControllerAdapter::getBlackList), + ; + + private String command; + private BiFunction function; + + MainMenu(String command, BiFunction function) { + this.command = command; + this.function = function; + } + + public String getCommand() { + return command; + } + + public ConsoleResponse execute(ControllerAdapter controller, Object... params) { + return function.apply(controller, params); + } + + public static ConsoleResponse routeToController(ConsoleRequest req, + ControllerAdapter controllerAdapter){ + return Stream.of(values()) + .filter(menuCommand -> menuCommand.getCommand().equals(req.getCommand())) + .findFirst() + .map(menuCommand -> { + if (req.getBody().isPresent()) { + return menuCommand.execute(controllerAdapter, req.getBody().get()); + } else { + return menuCommand.execute(controllerAdapter); + } + }) + .orElseThrow(() -> new CustomException(INVALID_MENU)); + } +} From f517b50afc47732f0dc72f59d27db56cc68a5d81 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:30:02 +0900 Subject: [PATCH 06/97] =?UTF-8?q?[feat]=20=EC=9A=94=EC=B2=AD=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provider/MenuRequestProvider.java | 27 +++++++++++++++++++ .../MenuRequestGenerator.java | 9 +++++++ .../RegisterVoucherGenerator.java | 27 +++++++++++++++++++ .../ViewAllBlackUserGenerator.java | 19 +++++++++++++ .../ViewAllVoucherGenerator.java | 19 +++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java diff --git a/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java b/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java new file mode 100644 index 0000000000..391b5b64d7 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.mediator.provider; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_MENU; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.mediator.requestGenerator.MenuRequestGenerator; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class MenuRequestProvider { + + List menuRequestGenerators; + + public MenuRequestProvider(List menuRequestGenerators) { + this.menuRequestGenerators = menuRequestGenerators; + } + + public ConsoleRequest getMenuRequest(String menuName) { + return menuRequestGenerators.stream() + .filter(menuRequestGenerator -> menuRequestGenerator.getMenuCommand().equals(menuName)) + .findFirst() + .map(MenuRequestGenerator::generateRequest) + .orElseThrow(() -> new CustomException(INVALID_MENU)); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java new file mode 100644 index 0000000000..f228164a38 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java @@ -0,0 +1,9 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.mediator.ConsoleRequest; + +public interface MenuRequestGenerator { + String getMenuCommand(); + + ConsoleRequest generateRequest(); +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java new file mode 100644 index 0000000000..4be73e9737 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class RegisterVoucherGenerator implements MenuRequestGenerator{ + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public RegisterVoucherGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.CREATE_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(),consoleInteractionAggregator.collectVoucherInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java new file mode 100644 index 0000000000..d294a3216f --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java @@ -0,0 +1,19 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class ViewAllBlackUserGenerator implements MenuRequestGenerator{ + + @Override + public String getMenuCommand() { + return MainMenu.LIST_BLACK_USER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest(getMenuCommand()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java new file mode 100644 index 0000000000..3caa171e95 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java @@ -0,0 +1,19 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class ViewAllVoucherGenerator implements MenuRequestGenerator{ + + @Override + public String getMenuCommand() { + return MainMenu.LIST_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest(getMenuCommand()); + } +} From 8dc1042b7384e62bf3142ba634beed670ef3bf86 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:30:41 +0900 Subject: [PATCH 07/97] =?UTF-8?q?[feat]=20=EB=A9=94=EC=84=B8=EC=A7=80=20co?= =?UTF-8?q?nstant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/util/Messages.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/util/Messages.java diff --git a/src/main/java/com/programmers/springbootbasic/util/Messages.java b/src/main/java/com/programmers/springbootbasic/util/Messages.java new file mode 100644 index 0000000000..4a78f1e1e4 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/util/Messages.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.util; + +public enum Messages { + + SELECT_MENU("=== Voucher Program ===" + System.lineSeparator() + + "종료 : exit" + System.lineSeparator() + + "바우처 등록 : create voucher" + System.lineSeparator() + + "바우처 조회 : list voucher" + System.lineSeparator() + + "유저 블랙리스트 조회 : list black user" + System.lineSeparator() + ), + VOUCHER_REGISTER_TYPE("등록할 바우처의 타입을 입력하세요.(FIXED, PERCENT)"), + VOUCHER_REGISTER_AMOUNT("등록할 바우처의 금액을 입력하세요.(숫자)"), + SUCCESS_VOUCHER_REGISTER("바우처가 등록되었습니다."), + SUCCESS_VOUCHER_LIST("바우처 목록입니다."), + SUCCESS_BLACK_USER_LIST("블랙리스트 유저 목록입니다."), + ; + + Messages(String Message) { + this.message = Message; + } + + private String message; + + public String getMessage() { + return message; + } +} From f96c831af1037e47fda25f7537ad513c89bdcee8 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:31:05 +0900 Subject: [PATCH 08/97] =?UTF-8?q?[feat]=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/UserService.java | 27 +++++++++++++++++ .../domain/user/domain/entity/User.java | 30 +++++++++++++++++++ .../user/presentation/UserController.java | 20 +++++++++++++ .../user/presentation/dto/UserResponse.java | 20 +++++++++++++ .../exceptionClass/UserException.java | 9 ++++++ 5 files changed, 106 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java new file mode 100644 index 0000000000..491900b834 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.domain.user.application; + +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import com.programmers.springbootbasic.util.FileManager; +import com.programmers.springbootbasic.util.FileProperties; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + private final FileManager fileManager; + + public UserService(FileManager fileManager) { + this.fileManager = fileManager; + } + + public List findBlacklistedUsers() { + return fileManager + .read(FileProperties.getUserFilePath(), User.class) + .stream() + .map(UserResponse::of) + .toList(); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java new file mode 100644 index 0000000000..4e551cc242 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java @@ -0,0 +1,30 @@ +package com.programmers.springbootbasic.domain.user.domain.entity; + +public class User { + private Long id; + private String name; + + public User(Long id, String name) { + this.id = id; + this.name = name; + } + + public User() { + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java new file mode 100644 index 0000000000..a99375b08c --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java @@ -0,0 +1,20 @@ +package com.programmers.springbootbasic.domain.user.presentation; + +import com.programmers.springbootbasic.domain.user.application.UserService; +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import java.util.List; +import org.springframework.stereotype.Controller; + +@Controller +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + public List getBlackList() { + return userService.findBlacklistedUsers(); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java new file mode 100644 index 0000000000..609a09dbd4 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java @@ -0,0 +1,20 @@ +package com.programmers.springbootbasic.domain.user.presentation.dto; + +import com.programmers.springbootbasic.domain.user.domain.entity.User; + +public class UserResponse { + private String name; + @Override + public String toString() { + return + "name =" + name + System.lineSeparator() + "===============================" + System.lineSeparator(); + } + + public UserResponse(String name) { + this.name = name; + } + + public static UserResponse of(User user) { + return new UserResponse(user.getName()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java new file mode 100644 index 0000000000..a7c1990064 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java @@ -0,0 +1,9 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class UserException extends RuntimeException{ + UserException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } +} From 515dcc3c2f032f4f1d3c508b4c547c1e8fd95ccc Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:31:49 +0900 Subject: [PATCH 09/97] =?UTF-8?q?[feat]=20voucher=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voucher/application/VoucherService.java | 29 ++++++++++++ .../voucher/domain/VoucherRepository.java | 11 +++++ .../VoucherType/FixedAmountVoucher.java | 24 ++++++++++ .../VoucherType/PercentDiscountVoucher.java | 24 ++++++++++ .../domain/VoucherType/VoucherType.java | 6 +++ .../domain/VoucherType/VoucherTypeEnum.java | 30 +++++++++++++ .../domain/voucher/domain/entity/Voucher.java | 39 ++++++++++++++++ .../voucher/infrastructure/CsvVoucher.java | 41 +++++++++++++++++ .../FilePersistenceVoucherRepository.java | 45 +++++++++++++++++++ .../MemoryVoucherRepository.java | 26 +++++++++++ .../presentation/VoucherController.java | 27 +++++++++++ .../dto/CreateVoucherRequest.java | 31 +++++++++++++ .../presentation/dto/VoucherResponse.java | 30 +++++++++++++ .../exceptionClass/VoucherException.java | 9 ++++ 14 files changed, 372 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java new file mode 100644 index 0000000000..1168eb4353 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java @@ -0,0 +1,29 @@ +package com.programmers.springbootbasic.domain.voucher.application; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class VoucherService { + + private final VoucherRepository voucherRepository; + + public VoucherService(VoucherRepository voucherRepository) { + this.voucherRepository = voucherRepository; + } + + public void createVoucher(CreateVoucherRequest request) { + // 중복 바우처 체크 + voucherRepository.save(request.toEntity()); + } + + public List findAllVouchers() { + return voucherRepository.findAll().stream() + .map(VoucherResponse::of) + .toList(); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java new file mode 100644 index 0000000000..fd4a484612 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java @@ -0,0 +1,11 @@ +package com.programmers.springbootbasic.domain.voucher.domain; + +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface VoucherRepository { + Voucher save(Voucher voucher); + List findAll(); +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java new file mode 100644 index 0000000000..0998ac375e --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java @@ -0,0 +1,24 @@ +package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_FIXED_VOUCHER_BENEFIT; + +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; + +public class FixedAmountVoucher implements VoucherType{ + + public FixedAmountVoucher(Integer benefit) { + if (benefit < 0) { + throw new VoucherException(INVALID_FIXED_VOUCHER_BENEFIT); + } + } + + @Override + public String getVoucherTypeName() { + return "FIXED"; + } + + @Override + public double getDiscountedPrice(double price, int benefitValue) { + return price - benefitValue; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java new file mode 100644 index 0000000000..ed99dee60f --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java @@ -0,0 +1,24 @@ +package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_PERCENT_VOUCHER_BENEFIT; + +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; + +public class PercentDiscountVoucher implements VoucherType{ + + public PercentDiscountVoucher(Integer benefit) { + if (benefit < 0 || benefit > 100) { + throw new VoucherException(INVALID_PERCENT_VOUCHER_BENEFIT); + } + } + + @Override + public String getVoucherTypeName() { + return "PERCENT"; + } + + @Override + public double getDiscountedPrice(double price, int benefitValue) { + return price * (100 - benefitValue) / 100; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java new file mode 100644 index 0000000000..52b46c70c9 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java @@ -0,0 +1,6 @@ +package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; + +public interface VoucherType { + String getVoucherTypeName(); + double getDiscountedPrice(double price, int benefitValue); +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java new file mode 100644 index 0000000000..1cf1c9dde2 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java @@ -0,0 +1,30 @@ +package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_VOUCHER; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import java.util.function.Function; +import java.util.function.Supplier; + +public enum VoucherTypeEnum { + FIXED(FixedAmountVoucher::new), + PERCENT(PercentDiscountVoucher::new); + + private Function voucherType; + + VoucherTypeEnum(Function voucherType) { + this.voucherType = voucherType; + } + + public VoucherType getVoucherType(Integer benefit) { + return voucherType.apply(benefit); + } + + public static VoucherTypeEnum of(String voucherType) { + try { + return VoucherTypeEnum.valueOf(voucherType.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CustomException(INVALID_VOUCHER); + } + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java new file mode 100644 index 0000000000..4045541db7 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java @@ -0,0 +1,39 @@ +package com.programmers.springbootbasic.domain.voucher.domain.entity; + + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; +import java.util.UUID; + +public class Voucher { + private UUID id; + private VoucherType voucherType; + private Integer benefitValue; + + public Voucher(UUID id, VoucherType voucherType, Integer benefitValue) { + this.id = id; + this.voucherType = voucherType; + this.benefitValue = benefitValue; + } + + public Voucher(VoucherType voucherType, Integer benefitValue) { + this.id = UUID.randomUUID(); + this.voucherType = voucherType; + this.benefitValue = benefitValue; + } + + public Voucher() { + } + + public UUID getId() { + return id; + } + + public VoucherType getVoucherType() { + return voucherType; + } + + public Integer getBenefitValue() { + return benefitValue; + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java new file mode 100644 index 0000000000..425a8a9c7d --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java @@ -0,0 +1,41 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.UUID; + +public class CsvVoucher { + private UUID id; + private String voucherType; + private Integer benefitValue; + + + public CsvVoucher() { + } + + public CsvVoucher(UUID id, String voucherType, Integer benefitValue) { + this.id = id; + this.voucherType = voucherType; + this.benefitValue = benefitValue; + } + + public UUID getId() { + return id; + } + + public String getVoucherType() { + return voucherType; + } + + public Integer getBenefitValue() { + return benefitValue; + } + + public static CsvVoucher of (Voucher voucher) { + return new CsvVoucher(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), voucher.getBenefitValue()); + } + + public Voucher toEntity() { + return new Voucher(id, VoucherTypeEnum.of(voucherType).getVoucherType(benefitValue), benefitValue); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java new file mode 100644 index 0000000000..d27cc02414 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java @@ -0,0 +1,45 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_FILE_PATH; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import com.programmers.springbootbasic.util.FileManager; +import com.programmers.springbootbasic.util.FileProperties; +import java.util.List; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +@Repository +@Profile("dev") +@Primary +public class FilePersistenceVoucherRepository implements VoucherRepository { + + private final List fileManagerList; + private FileManager fileManager; + + public FilePersistenceVoucherRepository(List fileManagerList) { + this.fileManagerList = fileManagerList; + fileManagerList.stream().filter((fm) -> fm.supports(FileProperties.getVoucherFilePath())) + .findFirst() + .ifPresentOrElse((fm) -> this.fileManager = fm, () -> { + throw new VoucherException(INVALID_FILE_PATH); + }); + } + + @Override + public Voucher save(Voucher voucher) { + fileManager.write(CsvVoucher.of(voucher), FileProperties.getVoucherFilePath()); + return voucher; + } + + @Override + public List findAll() { + var csvVouchers = fileManager.read(FileProperties.getVoucherFilePath(), CsvVoucher.class); + return csvVouchers.stream() + .map(CsvVoucher::toEntity) + .toList(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java new file mode 100644 index 0000000000..cb7c891f5f --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java @@ -0,0 +1,26 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.stereotype.Repository; + +@Repository +public class MemoryVoucherRepository implements VoucherRepository { + + ConcurrentHashMap vouchers = new ConcurrentHashMap<>(); + + @Override + public Voucher save(Voucher voucher) { + vouchers.put(voucher.getId(), voucher); + return voucher; + } + + @Override + public List findAll() { + return vouchers.values().stream().toList(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java new file mode 100644 index 0000000000..a48cc087cf --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.domain.voucher.presentation; + +import com.programmers.springbootbasic.domain.voucher.application.VoucherService; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import java.util.List; +import org.springframework.stereotype.Controller; + +@Controller +public class VoucherController { + + private final VoucherService voucherService; + + public VoucherController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + public void createVoucher(CreateVoucherRequest request) { + voucherService.createVoucher(request); + } + + public List getAllVouchers() { + return voucherService.findAllVouchers(); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java new file mode 100644 index 0000000000..758dfe3864 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -0,0 +1,31 @@ +package com.programmers.springbootbasic.domain.voucher.presentation.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; + +public class CreateVoucherRequest { + private VoucherTypeEnum voucherTypeEnum; + private Integer benefitValue; + + private CreateVoucherRequest(VoucherTypeEnum voucherTypeEnum, Integer benefitValue) { + this.voucherTypeEnum = voucherTypeEnum; + this.benefitValue = benefitValue; + } + + public VoucherTypeEnum getVoucherType() { + return voucherTypeEnum; + } + + public Integer getBenefitValue() { + return benefitValue; + } + + public static CreateVoucherRequest from(String voucherType, Integer benefitValue) { + VoucherTypeEnum voucherEnum = VoucherTypeEnum.of(voucherType); + return new CreateVoucherRequest(voucherEnum, benefitValue); + } + + public Voucher toEntity() { + return new Voucher(voucherTypeEnum.getVoucherType(benefitValue), benefitValue); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java new file mode 100644 index 0000000000..01459eecda --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java @@ -0,0 +1,30 @@ +package com.programmers.springbootbasic.domain.voucher.presentation.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.UUID; + +public class VoucherResponse { + private UUID id; + private String voucherType; + private Integer benefitValue; + + public VoucherResponse(UUID id, String voucherType, Integer benefitValue) { + this.id = id; + this.voucherType = voucherType; + this.benefitValue = benefitValue; + } + + @Override + public String toString() { + return + "Voucher id =" + id + System.lineSeparator() + + "Type =" + voucherType + System.lineSeparator() + + "benefitValue =" + benefitValue + System.lineSeparator() + + "===============================" + System.lineSeparator(); + } + + public static VoucherResponse of(Voucher voucher) { + return new VoucherResponse(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), voucher.getBenefitValue()); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java new file mode 100644 index 0000000000..d97a38139c --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java @@ -0,0 +1,9 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class VoucherException extends RuntimeException{ + public VoucherException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } +} From c3d862b1c425743219310b3e680d4365a9185330 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:32:09 +0900 Subject: [PATCH 10/97] =?UTF-8?q?[feat]=20=EC=98=88=EC=99=B8=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/VoucherApplication.java | 25 +++++++++ .../exception/AppExceptionHandler.java | 54 +++++++++++++++++++ .../springbootbasic/exception/ErrorCode.java | 21 ++++++++ .../exceptionClass/CustomException.java | 10 ++++ 4 files changed, 110 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/VoucherApplication.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java diff --git a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java new file mode 100644 index 0000000000..8026af4607 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java @@ -0,0 +1,25 @@ +package com.programmers.springbootbasic; + +import com.programmers.springbootbasic.exception.AppExceptionHandler; +import com.programmers.springbootbasic.infrastructure.IO.Console; +import com.programmers.springbootbasic.mediator.RequestProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +@SpringBootApplication +public class VoucherApplication { + + public static void main(String[] args) { + ApplicationContext applicationContext = SpringApplication.run(VoucherApplication.class, + args); + + AppExceptionHandler exceptionHandler = applicationContext.getBean( + AppExceptionHandler.class); + + while (true) { + exceptionHandler.handle(); + } + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java new file mode 100644 index 0000000000..54645d1feb --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java @@ -0,0 +1,54 @@ +package com.programmers.springbootbasic.exception; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.exception.exceptionClass.SystemException; +import com.programmers.springbootbasic.exception.exceptionClass.UserException; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import com.programmers.springbootbasic.mediator.ConsoleResponse; +import com.programmers.springbootbasic.mediator.RequestProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class AppExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(AppExceptionHandler.class); + private final RequestProcessor requestProcessor; + + public AppExceptionHandler(RequestProcessor requestProcessor) { + this.requestProcessor = requestProcessor; + } + + public void handle() { + try { + requestProcessor.run(); + } catch (SystemException e) { + requestProcessor.sendResponse( + new ConsoleResponse(e.getMessage()) + ); + logger.error("System Error" + e); + System.exit(1); + } catch (CustomException e) { + logger.error("Custom Error" + e); + requestProcessor.sendResponse( + new ConsoleResponse(e.getMessage()) + ); + } catch (VoucherException e) { + logger.error("Voucher Error" + e); + requestProcessor.sendResponse( + new ConsoleResponse(e.getMessage()) + ); + } catch (UserException e) { + logger.error("User Error" + e); + requestProcessor.sendResponse( + new ConsoleResponse(e.getMessage()) + ); + } catch (Exception e) { + logger.error("Exception Error" + e); + requestProcessor.sendResponse( + new ConsoleResponse(e.getMessage()) + ); + } + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java new file mode 100644 index 0000000000..91ec5c2c73 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -0,0 +1,21 @@ +package com.programmers.springbootbasic.exception; + +public enum ErrorCode { + FILE_IO_ERROR("파일 에러입니다. 시스템을 종료합니다."), + INVALID_MENU("올바르지 않은 메뉴 타입입니다."), + INVALID_VOUCHER("올바르지 않은 바우처 값 입니다."), + INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다."), + INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다."), + INVALID_FILE_PATH("올바르지 않은 파일 경로입니다."), + ; + + ErrorCode(String message) { + this.message = message; + } + + private final String message; + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java new file mode 100644 index 0000000000..da93bef3bf --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class CustomException extends RuntimeException { + + public CustomException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } +} From 042b79048c0fb0903793a3beb97fd8fa04a1f1e2 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:32:31 +0900 Subject: [PATCH 11/97] =?UTF-8?q?[chore]=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/util/FileProperties.java | 18 ++++++++++++++++++ src/main/resources/application.yml | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/util/FileProperties.java create mode 100644 src/main/resources/application.yml diff --git a/src/main/java/com/programmers/springbootbasic/util/FileProperties.java b/src/main/java/com/programmers/springbootbasic/util/FileProperties.java new file mode 100644 index 0000000000..3910b913bc --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/util/FileProperties.java @@ -0,0 +1,18 @@ +package com.programmers.springbootbasic.util; + +import org.springframework.beans.factory.annotation.Value; + +public class FileProperties { + @Value("${file.user.path}") + private static String UserFilePath; + @Value("${file.voucher.path}") + private static String VoucherFilePath; + + public static String getUserFilePath() { + return UserFilePath; + } + + public static String getVoucherFilePath() { + return VoucherFilePath; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000000..ac5234a3a5 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,5 @@ +file: + user: + path: blacklist.csv + voucher: + path: voucher.csv \ No newline at end of file From 42f17d4e8fdc7748ce8c6bac08754e07bde70dbc Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:32:44 +0900 Subject: [PATCH 12/97] =?UTF-8?q?[chore]=20logback=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/resources/logback-spring.xml diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000..6c11b15e1e --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + ERROR + ACCEPT + DENY + + + logs/error-%d{yyyy-MM-dd}.log + + + ${FILE_LOG_PATTERN} + + + + + + + + + From 9114d187809c70aec5d51fe0f52aed147c3f3230 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 23:49:16 +0900 Subject: [PATCH 13/97] =?UTF-8?q?[fix]=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=84=A4=EC=A0=95=EC=9D=B4=20=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/UserService.java | 8 +++++--- .../FilePersistenceVoucherRepository.java | 12 +++++++----- .../springbootbasic/util/FileProperties.java | 18 ------------------ 3 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 src/main/java/com/programmers/springbootbasic/util/FileProperties.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java index 491900b834..0d65f60b08 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java @@ -3,22 +3,24 @@ import com.programmers.springbootbasic.domain.user.domain.entity.User; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; import com.programmers.springbootbasic.util.FileManager; -import com.programmers.springbootbasic.util.FileProperties; import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class UserService { private final FileManager fileManager; + private String fileName; - public UserService(FileManager fileManager) { + public UserService(FileManager fileManager, @Value("${file.user.path}") String fileName) { + this.fileName = fileName; this.fileManager = fileManager; } public List findBlacklistedUsers() { return fileManager - .read(FileProperties.getUserFilePath(), User.class) + .read(fileName, User.class) .stream() .map(UserResponse::of) .toList(); diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java index d27cc02414..b6e067a592 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java @@ -6,8 +6,8 @@ import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; import com.programmers.springbootbasic.util.FileManager; -import com.programmers.springbootbasic.util.FileProperties; import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; @@ -19,10 +19,12 @@ public class FilePersistenceVoucherRepository implements VoucherRepository { private final List fileManagerList; private FileManager fileManager; + private String fileName; - public FilePersistenceVoucherRepository(List fileManagerList) { + public FilePersistenceVoucherRepository(List fileManagerList, @Value("${file.voucher.path}") String fileName) { this.fileManagerList = fileManagerList; - fileManagerList.stream().filter((fm) -> fm.supports(FileProperties.getVoucherFilePath())) + this.fileName = fileName; + fileManagerList.stream().filter((fm) -> fm.supports(fileName)) .findFirst() .ifPresentOrElse((fm) -> this.fileManager = fm, () -> { throw new VoucherException(INVALID_FILE_PATH); @@ -31,13 +33,13 @@ public FilePersistenceVoucherRepository(List fileManagerList) { @Override public Voucher save(Voucher voucher) { - fileManager.write(CsvVoucher.of(voucher), FileProperties.getVoucherFilePath()); + fileManager.write(CsvVoucher.of(voucher), fileName); return voucher; } @Override public List findAll() { - var csvVouchers = fileManager.read(FileProperties.getVoucherFilePath(), CsvVoucher.class); + var csvVouchers = fileManager.read(fileName, CsvVoucher.class); return csvVouchers.stream() .map(CsvVoucher::toEntity) .toList(); diff --git a/src/main/java/com/programmers/springbootbasic/util/FileProperties.java b/src/main/java/com/programmers/springbootbasic/util/FileProperties.java deleted file mode 100644 index 3910b913bc..0000000000 --- a/src/main/java/com/programmers/springbootbasic/util/FileProperties.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.programmers.springbootbasic.util; - -import org.springframework.beans.factory.annotation.Value; - -public class FileProperties { - @Value("${file.user.path}") - private static String UserFilePath; - @Value("${file.voucher.path}") - private static String VoucherFilePath; - - public static String getUserFilePath() { - return UserFilePath; - } - - public static String getVoucherFilePath() { - return VoucherFilePath; - } -} \ No newline at end of file From 58b9b4b19f7fb8d0891a9d0059c5d89ff3ffb4a5 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 20 Oct 2023 23:52:02 +0900 Subject: [PATCH 14/97] =?UTF-8?q?[change]=20=EC=9C=A0=ED=9A=A8=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EB=B0=94=EC=9A=B0=EC=B2=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/programmers/springbootbasic/exception/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java index 91ec5c2c73..9cb408068f 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -3,7 +3,7 @@ public enum ErrorCode { FILE_IO_ERROR("파일 에러입니다. 시스템을 종료합니다."), INVALID_MENU("올바르지 않은 메뉴 타입입니다."), - INVALID_VOUCHER("올바르지 않은 바우처 값 입니다."), + INVALID_VOUCHER("올바르지 않은 바우처 타입 입니다. 현재 등록 가능한 바우처 타입은 FIXED, PERCENT 입니다."), INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다."), INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다."), INVALID_FILE_PATH("올바르지 않은 파일 경로입니다."), From d2eb034d343569690f38b7651ce7dbef5c385c51 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:39:32 +0900 Subject: [PATCH 15/97] =?UTF-8?q?[change]=20toString=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=A7=A8=20=EB=B0=91=EC=9C=BC=EB=A1=9C=20+=20name?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20final=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/presentation/dto/UserResponse.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java index 609a09dbd4..148b6fb7db 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java @@ -3,12 +3,7 @@ import com.programmers.springbootbasic.domain.user.domain.entity.User; public class UserResponse { - private String name; - @Override - public String toString() { - return - "name =" + name + System.lineSeparator() + "===============================" + System.lineSeparator(); - } + private final String name; public UserResponse(String name) { this.name = name; @@ -17,4 +12,12 @@ public UserResponse(String name) { public static UserResponse of(User user) { return new UserResponse(user.getName()); } + + @Override + public String toString() { + return """ + name = %s + =============================== + """.formatted(name); + } } From 74dd3af8b84e93cc0b3a9201a48245ddf05afedd Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:39:47 +0900 Subject: [PATCH 16/97] =?UTF-8?q?[change]=20fileName=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20final=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/domain/user/application/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java index 0d65f60b08..ea0fba1e37 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java @@ -11,7 +11,7 @@ public class UserService { private final FileManager fileManager; - private String fileName; + private final String fileName; public UserService(FileManager fileManager, @Value("${file.user.path}") String fileName) { this.fileName = fileName; From 352eb8a03ea4e65bde246219f9421e6d495e21d0 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:44:19 +0900 Subject: [PATCH 17/97] =?UTF-8?q?[change]=20message=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20final=20=EC=B2=98=EB=A6=AC=20+=20TextBlock=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/util/Messages.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/util/Messages.java b/src/main/java/com/programmers/springbootbasic/util/Messages.java index 4a78f1e1e4..0489b78332 100644 --- a/src/main/java/com/programmers/springbootbasic/util/Messages.java +++ b/src/main/java/com/programmers/springbootbasic/util/Messages.java @@ -2,12 +2,13 @@ public enum Messages { - SELECT_MENU("=== Voucher Program ===" + System.lineSeparator() - + "종료 : exit" + System.lineSeparator() - + "바우처 등록 : create voucher" + System.lineSeparator() - + "바우처 조회 : list voucher" + System.lineSeparator() - + "유저 블랙리스트 조회 : list black user" + System.lineSeparator() - ), + SELECT_MENU(""" + === Voucher Program === + 종료 : exit + 바우처 등록 : create voucher + 바우처 조회 : list voucher + 유저 블랙리스트 조회 : list black user + """), VOUCHER_REGISTER_TYPE("등록할 바우처의 타입을 입력하세요.(FIXED, PERCENT)"), VOUCHER_REGISTER_AMOUNT("등록할 바우처의 금액을 입력하세요.(숫자)"), SUCCESS_VOUCHER_REGISTER("바우처가 등록되었습니다."), @@ -19,7 +20,7 @@ public enum Messages { this.message = Message; } - private String message; + private final String message; public String getMessage() { return message; From b02f208cfde71ab38b4e085157cab5bfd09f8073 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:54:18 +0900 Subject: [PATCH 18/97] =?UTF-8?q?[change]=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A1=9C=EB=94=A9=20this()=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=20+=20=EA=B8=B0=EB=B3=B8=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/domain/entity/Voucher.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java index 4045541db7..c4019c77ba 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java @@ -5,23 +5,19 @@ import java.util.UUID; public class Voucher { - private UUID id; - private VoucherType voucherType; - private Integer benefitValue; - public Voucher(UUID id, VoucherType voucherType, Integer benefitValue) { - this.id = id; - this.voucherType = voucherType; - this.benefitValue = benefitValue; - } + private final UUID id; + private final VoucherType voucherType; + private final Integer benefitValue; public Voucher(VoucherType voucherType, Integer benefitValue) { - this.id = UUID.randomUUID(); - this.voucherType = voucherType; - this.benefitValue = benefitValue; + this(UUID.randomUUID(), voucherType, benefitValue); } - public Voucher() { + public Voucher(UUID id, VoucherType voucherType, Integer benefitValue) { + this.id = id; + this.voucherType = voucherType; + this.benefitValue = benefitValue; } public UUID getId() { From ca62e69d5626e887b7d7bf2897c8a7b2cea7de74 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:59:15 +0900 Subject: [PATCH 19/97] =?UTF-8?q?[change]=20enum=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=83=81=EC=88=98=EA=B0=92=20=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/domain/VoucherType/FixedAmountVoucher.java | 3 ++- .../voucher/domain/VoucherType/PercentDiscountVoucher.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java index 0998ac375e..16c63ce0f1 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java @@ -1,5 +1,6 @@ package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; +import static com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum.FIXED; import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_FIXED_VOUCHER_BENEFIT; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; @@ -14,7 +15,7 @@ public FixedAmountVoucher(Integer benefit) { @Override public String getVoucherTypeName() { - return "FIXED"; + return FIXED.name(); } @Override diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java index ed99dee60f..6f9512e05d 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java @@ -1,5 +1,6 @@ package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; +import static com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum.PERCENT; import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_PERCENT_VOUCHER_BENEFIT; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; @@ -14,7 +15,7 @@ public PercentDiscountVoucher(Integer benefit) { @Override public String getVoucherTypeName() { - return "PERCENT"; + return PERCENT.name(); } @Override From de2cac3c41a6fd16e570d7c20d659a08ab07e711 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:00:37 +0900 Subject: [PATCH 20/97] =?UTF-8?q?[change]=20=ED=95=A8=EC=88=98=20final=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/domain/VoucherType/VoucherTypeEnum.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java index 1cf1c9dde2..c78a232436 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java @@ -4,13 +4,12 @@ import com.programmers.springbootbasic.exception.exceptionClass.CustomException; import java.util.function.Function; -import java.util.function.Supplier; public enum VoucherTypeEnum { FIXED(FixedAmountVoucher::new), PERCENT(PercentDiscountVoucher::new); - private Function voucherType; + private final Function voucherType; VoucherTypeEnum(Function voucherType) { this.voucherType = voucherType; From dd138943e77d7dbf5aff52cbe0565d5aa80afdc1 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:04:10 +0900 Subject: [PATCH 21/97] =?UTF-8?q?[change]=20=EB=A1=9C=EC=BB=AC=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=ED=99=94=20+=20=ED=95=84=EB=93=9C=20final=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilePersistenceVoucherRepository.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java index b6e067a592..33532994e4 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java @@ -17,16 +17,15 @@ @Primary public class FilePersistenceVoucherRepository implements VoucherRepository { - private final List fileManagerList; private FileManager fileManager; - private String fileName; + private final String fileName; - public FilePersistenceVoucherRepository(List fileManagerList, @Value("${file.voucher.path}") String fileName) { - this.fileManagerList = fileManagerList; + public FilePersistenceVoucherRepository(List fileManagerList, + @Value("${file.voucher.path}") String fileName) { this.fileName = fileName; - fileManagerList.stream().filter((fm) -> fm.supports(fileName)) + fileManagerList.stream().filter(fm -> fm.supports(fileName)) .findFirst() - .ifPresentOrElse((fm) -> this.fileManager = fm, () -> { + .ifPresentOrElse(fm -> this.fileManager = fm, () -> { throw new VoucherException(INVALID_FILE_PATH); }); } From 36156ee42eb0d4a2e575ff8c057e2a3e8fa58915 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:06:17 +0900 Subject: [PATCH 22/97] =?UTF-8?q?[change]=20=EC=A0=91=EA=B7=BC=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EC=9E=90=20private=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/infrastructure/MemoryVoucherRepository.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java index cb7c891f5f..7b55ca53b8 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java @@ -3,7 +3,6 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Repository; @@ -11,7 +10,7 @@ @Repository public class MemoryVoucherRepository implements VoucherRepository { - ConcurrentHashMap vouchers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap vouchers = new ConcurrentHashMap<>(); @Override public Voucher save(Voucher voucher) { From 72e58c72a0cc3276a71a955b92116aa7f2d807d1 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:08:54 +0900 Subject: [PATCH 23/97] =?UTF-8?q?[change]=20=ED=95=84=EB=93=9C=20final=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/presentation/dto/CreateVoucherRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index 758dfe3864..4a74dfaa29 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -4,8 +4,8 @@ import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; public class CreateVoucherRequest { - private VoucherTypeEnum voucherTypeEnum; - private Integer benefitValue; + private final VoucherTypeEnum voucherTypeEnum; + private final Integer benefitValue; private CreateVoucherRequest(VoucherTypeEnum voucherTypeEnum, Integer benefitValue) { this.voucherTypeEnum = voucherTypeEnum; From 1b94312b83d33c27bdf1cb51f91ed411ef63e6ef Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:11:42 +0900 Subject: [PATCH 24/97] =?UTF-8?q?[change]=20=ED=95=84=EB=93=9C=20final=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20+=20TextBlock=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/dto/VoucherResponse.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java index 01459eecda..b542b3d915 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java @@ -4,9 +4,10 @@ import java.util.UUID; public class VoucherResponse { - private UUID id; - private String voucherType; - private Integer benefitValue; + + private final UUID id; + private final String voucherType; + private final Integer benefitValue; public VoucherResponse(UUID id, String voucherType, Integer benefitValue) { this.id = id; @@ -16,15 +17,17 @@ public VoucherResponse(UUID id, String voucherType, Integer benefitValue) { @Override public String toString() { - return - "Voucher id =" + id + System.lineSeparator() + - "Type =" + voucherType + System.lineSeparator() + - "benefitValue =" + benefitValue + System.lineSeparator() + - "===============================" + System.lineSeparator(); + return """ + Voucher id = %s + Type = %s + benefitValue = %s + =============================== + """.formatted(id, voucherType, benefitValue); } public static VoucherResponse of(Voucher voucher) { - return new VoucherResponse(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), voucher.getBenefitValue()); + return new VoucherResponse(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), + voucher.getBenefitValue()); } } From 3b1b2d2310704c32f6681d8a231f302ca64e2abe Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:20:17 +0900 Subject: [PATCH 25/97] =?UTF-8?q?[change]=20=ED=95=84=EB=93=9C=20final=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20+=20=EC=98=A4=EB=B2=84=EB=A1=9C=EB=94=A9?= =?UTF-8?q?=20this()=20=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/mediator/ConsoleRequest.java | 9 +++++---- .../springbootbasic/mediator/ConsoleResponse.java | 13 +++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java index 4d086cc1e6..0d5dff2698 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleRequest.java @@ -3,14 +3,15 @@ import java.util.Optional; public class ConsoleRequest { - private String command; - private T body; + + private final String command; + private final T body; public ConsoleRequest(String command) { - this.command = command; + this(command, null); } - public ConsoleRequest(String command,T body) { + public ConsoleRequest(String command, T body) { this.command = command; this.body = body; } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java index 61fecb0116..b61c0a33d0 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java @@ -3,15 +3,16 @@ import java.util.Optional; public class ConsoleResponse { - private T body; - private String message; - public ConsoleResponse(T body, String message) { - this.body = body; - this.message = message; - } + private final String message; + private final T body; public ConsoleResponse(String message) { + this(null, message); + } + + public ConsoleResponse(T body, String message) { + this.body = body; this.message = message; } From 618c212bea6903d9fe6e05df2382d8238cc414c9 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:28:59 +0900 Subject: [PATCH 26/97] =?UTF-8?q?[change]=20=ED=95=A8=EC=88=98=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voucher/infrastructure/CsvVoucher.java | 20 ++++++++++--------- .../dto/CreateVoucherRequest.java | 16 +++++++-------- .../presentation/dto/VoucherResponse.java | 10 +++++----- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java index 425a8a9c7d..59e51e65f7 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java @@ -5,11 +5,11 @@ import java.util.UUID; public class CsvVoucher { + private UUID id; private String voucherType; private Integer benefitValue; - public CsvVoucher() { } @@ -19,6 +19,16 @@ public CsvVoucher(UUID id, String voucherType, Integer benefitValue) { this.benefitValue = benefitValue; } + public static CsvVoucher of(Voucher voucher) { + return new CsvVoucher(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), + voucher.getBenefitValue()); + } + + public Voucher toEntity() { + return new Voucher(id, VoucherTypeEnum.of(voucherType).getVoucherType(benefitValue), + benefitValue); + } + public UUID getId() { return id; } @@ -30,12 +40,4 @@ public String getVoucherType() { public Integer getBenefitValue() { return benefitValue; } - - public static CsvVoucher of (Voucher voucher) { - return new CsvVoucher(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), voucher.getBenefitValue()); - } - - public Voucher toEntity() { - return new Voucher(id, VoucherTypeEnum.of(voucherType).getVoucherType(benefitValue), benefitValue); - } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index 4a74dfaa29..c9934e00f8 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -12,14 +12,6 @@ private CreateVoucherRequest(VoucherTypeEnum voucherTypeEnum, Integer benefitVal this.benefitValue = benefitValue; } - public VoucherTypeEnum getVoucherType() { - return voucherTypeEnum; - } - - public Integer getBenefitValue() { - return benefitValue; - } - public static CreateVoucherRequest from(String voucherType, Integer benefitValue) { VoucherTypeEnum voucherEnum = VoucherTypeEnum.of(voucherType); return new CreateVoucherRequest(voucherEnum, benefitValue); @@ -28,4 +20,12 @@ public static CreateVoucherRequest from(String voucherType, Integer benefitValue public Voucher toEntity() { return new Voucher(voucherTypeEnum.getVoucherType(benefitValue), benefitValue); } + + public VoucherTypeEnum getVoucherType() { + return voucherTypeEnum; + } + + public Integer getBenefitValue() { + return benefitValue; + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java index b542b3d915..2fc27d229a 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java @@ -15,6 +15,11 @@ public VoucherResponse(UUID id, String voucherType, Integer benefitValue) { this.benefitValue = benefitValue; } + public static VoucherResponse of(Voucher voucher) { + return new VoucherResponse(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), + voucher.getBenefitValue()); + } + @Override public String toString() { return """ @@ -25,9 +30,4 @@ public String toString() { """.formatted(id, voucherType, benefitValue); } - public static VoucherResponse of(Voucher voucher) { - return new VoucherResponse(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), - voucher.getBenefitValue()); - } - } From 569ba3365a28637e7cfe036c74776ee8f2ae4906 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 04:10:49 +0900 Subject: [PATCH 27/97] =?UTF-8?q?[change]=20csv=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20->=20=EC=88=9C=EC=88=98=20?= =?UTF-8?q?=EC=9E=90=EB=B0=94=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 - .../voucher/infrastructure/CsvVoucher.java | 20 ++-- .../springbootbasic/util/CsvManager.java | 106 +++++++++++++----- 3 files changed, 86 insertions(+), 42 deletions(-) diff --git a/build.gradle b/build.gradle index 566788c652..a15d30e1ed 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' - - implementation 'com.opencsv:opencsv:5.8' } tasks.named('test') { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java index 59e51e65f7..00cf38d837 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java @@ -6,30 +6,32 @@ public class CsvVoucher { - private UUID id; + private String id; private String voucherType; - private Integer benefitValue; + private String benefitValue; public CsvVoucher() { } - public CsvVoucher(UUID id, String voucherType, Integer benefitValue) { + public CsvVoucher(String id, String voucherType, String benefitValue) { this.id = id; this.voucherType = voucherType; this.benefitValue = benefitValue; } public static CsvVoucher of(Voucher voucher) { - return new CsvVoucher(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), - voucher.getBenefitValue()); + return new CsvVoucher(voucher.getId().toString(), + voucher.getVoucherType().getVoucherTypeName(), + voucher.getBenefitValue().toString()); } public Voucher toEntity() { - return new Voucher(id, VoucherTypeEnum.of(voucherType).getVoucherType(benefitValue), - benefitValue); + return new Voucher(UUID.fromString(id), + VoucherTypeEnum.of(voucherType).getVoucherType(Integer.valueOf(benefitValue)), + Integer.valueOf(benefitValue)); } - public UUID getId() { + public String getId() { return id; } @@ -37,7 +39,7 @@ public String getVoucherType() { return voucherType; } - public Integer getBenefitValue() { + public String getBenefitValue() { return benefitValue; } } diff --git a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java index 9ce9cdb7ff..5026b0a75a 100644 --- a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java +++ b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java @@ -3,18 +3,15 @@ import static com.programmers.springbootbasic.exception.ErrorCode.FILE_IO_ERROR; -import com.opencsv.bean.CsvToBean; -import com.opencsv.bean.CsvToBeanBuilder; -import com.opencsv.bean.StatefulBeanToCsv; -import com.opencsv.bean.StatefulBeanToCsvBuilder; -import com.opencsv.exceptions.CsvDataTypeMismatchException; -import com.opencsv.exceptions.CsvRequiredFieldEmptyException; +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; import com.programmers.springbootbasic.exception.exceptionClass.SystemException; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.Reader; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -23,7 +20,7 @@ import org.springframework.stereotype.Component; @Component -public class CsvManager implements FileManager{ +public class CsvManager implements FileManager { private static final Set EXTENSION = Set.of("csv"); @@ -34,48 +31,95 @@ public boolean supports(String fileName) { } @Override - public List read(String fileName, Class type){ - Resource resource = new PathResource(fileName); + public List read(String fileName, Class type) { + File file = getFileResource(fileName); + List resultList = new ArrayList<>(); - if (!resource.exists()) { - return new ArrayList<>(); // 파일이 없으면 빈 리스트 반환 + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + + // 헤더 스킵 (첫 줄 읽기) + reader.readLine(); + + while ((line = reader.readLine()) != null) { + String[] values = line.split(","); + T obj = parseToObject(values, type); + resultList.add(obj); + } + } catch (IOException | ReflectiveOperationException e) { + throw new CustomException(FILE_IO_ERROR); } - try (Reader reader = new FileReader(resource.getFile())) { - // CsvToBean 객체 생성 및 파싱 - CsvToBean csvToBean = new CsvToBeanBuilder(reader) - .withType(type) - .withIgnoreLeadingWhiteSpace(true) - .build(); + return resultList; + } - return csvToBean.parse(); + @Override + public void write(T entity, String fileName) { + File file = getFileResource(fileName); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file, true))) { + if (file.length() == 0) { + writeHeaders(writer, entity); + } + writeEntity(writer, entity); } catch (IOException e) { - System.out.println(e.getMessage()); throw new SystemException(FILE_IO_ERROR); } } - @Override - public void write(T entity, String fileName) { + private T parseToObject(String[] values, Class type) + throws ReflectiveOperationException { + T instance = type.getDeclaredConstructor().newInstance(); + + Field[] fields = type.getDeclaredFields(); + for (int i = 0; i < values.length; i++) { + if (i < fields.length) { + Field field = fields[i]; + field.setAccessible(true); + field.set(instance, values[i]); + } + } + return instance; + } + + private File getFileResource(String fileName) { try { Resource resource = new PathResource(fileName); - File file = null; if (!resource.exists()) { resource.getFile().createNewFile(); } - file = resource.getFile(); - try (FileWriter writer = new FileWriter(file)) { - StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer) - .withSeparator(',') - .build(); + return resource.getFile(); + } catch (IOException e) { + throw new SystemException(FILE_IO_ERROR); + } + } - beanToCsv.write(entity); + private void writeHeaders(BufferedWriter writer, T entity) throws IOException { + Field[] fields = entity.getClass().getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + writer.write(fields[i].getName()); + if (i < fields.length - 1) { + writer.write(","); } - } catch (IOException | CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) { - throw new SystemException(FILE_IO_ERROR); } + writer.newLine(); } + private void writeEntity(BufferedWriter writer, T entity) throws IOException { + Field[] fields = entity.getClass().getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + fields[i].setAccessible(true); + try { + Object value = fields[i].get(entity); + writer.write(value != null ? value.toString() : ""); + } catch (IllegalAccessException e) { + throw new SystemException(FILE_IO_ERROR); + } + if (i < fields.length - 1) { + writer.write(","); + } + } + writer.newLine(); + } } From 29a6082de6b62102440fcfa6dffc5576ebb0eb5f Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:18:54 +0900 Subject: [PATCH 28/97] =?UTF-8?q?[change]=20=EC=A2=85=EB=A3=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20graceful=20shutdown=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/VoucherApplication.java | 9 ++-- .../exception/AppExceptionHandler.java | 41 +++++++++++++------ .../springbootbasic/exception/ErrorCode.java | 1 + .../exceptionClass/SystemException.java | 14 +++++-- .../ExitRequestGenerator.java | 17 ++++++++ .../presentation/ControllerAdapter.java | 6 +++ .../presentation/MainMenu.java | 1 + src/main/resources/application.yml | 5 +++ 8 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java diff --git a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java index 8026af4607..b1702ba586 100644 --- a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java +++ b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java @@ -6,20 +6,23 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class VoucherApplication { public static void main(String[] args) { - ApplicationContext applicationContext = SpringApplication.run(VoucherApplication.class, + ConfigurableApplicationContext applicationContext = SpringApplication.run( + VoucherApplication.class, args); AppExceptionHandler exceptionHandler = applicationContext.getBean( AppExceptionHandler.class); - while (true) { + // 컨텍스트가 종료된 경우 + do { exceptionHandler.handle(); - } + } while (applicationContext.isActive()); } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java index 54645d1feb..f24cb64e75 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java @@ -1,6 +1,7 @@ package com.programmers.springbootbasic.exception; import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.exception.exceptionClass.FileIOException; import com.programmers.springbootbasic.exception.exceptionClass.SystemException; import com.programmers.springbootbasic.exception.exceptionClass.UserException; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; @@ -8,6 +9,8 @@ import com.programmers.springbootbasic.mediator.RequestProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Component; @Component @@ -15,40 +18,54 @@ public class AppExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(AppExceptionHandler.class); private final RequestProcessor requestProcessor; + private final ConfigurableApplicationContext context; - public AppExceptionHandler(RequestProcessor requestProcessor) { + public AppExceptionHandler(RequestProcessor requestProcessor, + ConfigurableApplicationContext context) { this.requestProcessor = requestProcessor; + this.context = context; } public void handle() { try { requestProcessor.run(); } catch (SystemException e) { + logger.error(String.format("System Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + new ConsoleResponse(e.getMessage()) + ); + handleExit(e); + } catch (FileIOException e) { + logger.error(String.format("FileIO Error : %s", e.getMessage())); + requestProcessor.sendResponse( + new ConsoleResponse(e.getMessage()) ); - logger.error("System Error" + e); - System.exit(1); } catch (CustomException e) { - logger.error("Custom Error" + e); + logger.error(String.format("Custom Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + new ConsoleResponse(e.getMessage()) ); } catch (VoucherException e) { - logger.error("Voucher Error" + e); + logger.error(String.format("Voucher Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + new ConsoleResponse(e.getMessage()) ); } catch (UserException e) { - logger.error("User Error" + e); + logger.error(String.format("User Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + new ConsoleResponse(e.getMessage()) ); } catch (Exception e) { - logger.error("Exception Error" + e); + logger.error(String.format("Unknown Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + new ConsoleResponse(e.getMessage()) ); } } + + private void handleExit(SystemException e) { + if (e.getErrorCode().equals(ErrorCode.EXIT)) { + context.close(); + } + } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java index 9cb408068f..7a8c589d1d 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -7,6 +7,7 @@ public enum ErrorCode { INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다."), INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다."), INVALID_FILE_PATH("올바르지 않은 파일 경로입니다."), + EXIT("시스템을 종료합니다."), ; ErrorCode(String message) { diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java index f186d2b9d8..9a582d0a99 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java @@ -2,9 +2,15 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class SystemException extends RuntimeException{ +public class SystemException extends RuntimeException { + private final ErrorCode errorCode; - public SystemException(ErrorCode errorCode) { - super(errorCode.getMessage()); - } + public SystemException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return this.errorCode; + } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java new file mode 100644 index 0000000000..aaf033b009 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java @@ -0,0 +1,17 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; + +public class ExitRequestGenerator implements MenuRequestGenerator{ + + @Override + public String getMenuCommand() { + return MainMenu.EXIT.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest(getMenuCommand()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java index de16903b79..ed64ae1f93 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -1,5 +1,6 @@ package com.programmers.springbootbasic.presentation; +import static com.programmers.springbootbasic.exception.ErrorCode.EXIT; import static com.programmers.springbootbasic.util.Messages.SUCCESS_BLACK_USER_LIST; import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_LIST; import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_REGISTER; @@ -7,6 +8,7 @@ import com.programmers.springbootbasic.domain.user.presentation.UserController; import com.programmers.springbootbasic.domain.voucher.presentation.VoucherController; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.exception.exceptionClass.SystemException; import com.programmers.springbootbasic.mediator.ConsoleResponse; import org.springframework.stereotype.Component; @@ -21,6 +23,10 @@ public ControllerAdapter(VoucherController voucherController, UserController use this.userController = userController; } + public ConsoleResponse handleExit(Object... params) { + throw new SystemException(EXIT); + } + public ConsoleResponse createVoucher(Object... params) { CreateVoucherRequest request = (CreateVoucherRequest) params[0]; voucherController.createVoucher(request); diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index 421559f88d..8163618b39 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -9,6 +9,7 @@ import java.util.stream.Stream; public enum MainMenu { + EXIT("exit",ControllerAdapter::handleExit), CREATE_VOUCHER("create voucher",ControllerAdapter::createVoucher), LIST_VOUCHER("list voucher",ControllerAdapter::getAllVouchers), LIST_BLACK_USER("list black user",ControllerAdapter::getBlackList), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ac5234a3a5..35f15831f0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,8 @@ +server: + shutdown: graceful +spring: + lifecycle: + timeout-per-shutdown-phase: 10s file: user: path: blacklist.csv From 116ef3338815b7fb38744718daf2ca186c2d3e74 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:19:13 +0900 Subject: [PATCH 29/97] =?UTF-8?q?[change]=20=ED=8C=8C=EC=9D=BC=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=95=B1=20=EC=A2=85=EB=A3=8C=20=ED=95=98=EC=A7=80?= =?UTF-8?q?=20=EC=95=8A=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/exceptionClass/FileIOException.java | 10 ++++++++++ .../programmers/springbootbasic/util/CsvManager.java | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java new file mode 100644 index 0000000000..4cb3e6619a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class FileIOException extends RuntimeException{ + + public FileIOException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java index 5026b0a75a..ef1ccf3cdd 100644 --- a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java +++ b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java @@ -4,7 +4,7 @@ import static com.programmers.springbootbasic.exception.ErrorCode.FILE_IO_ERROR; import com.programmers.springbootbasic.exception.exceptionClass.CustomException; -import com.programmers.springbootbasic.exception.exceptionClass.SystemException; +import com.programmers.springbootbasic.exception.exceptionClass.FileIOException; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -62,7 +62,7 @@ public void write(T entity, String fileName) { } writeEntity(writer, entity); } catch (IOException e) { - throw new SystemException(FILE_IO_ERROR); + throw new FileIOException(FILE_IO_ERROR); } } @@ -89,7 +89,7 @@ private File getFileResource(String fileName) { } return resource.getFile(); } catch (IOException e) { - throw new SystemException(FILE_IO_ERROR); + throw new FileIOException(FILE_IO_ERROR); } } @@ -112,7 +112,7 @@ private void writeEntity(BufferedWriter writer, T entity) throws IOException Object value = fields[i].get(entity); writer.write(value != null ? value.toString() : ""); } catch (IllegalAccessException e) { - throw new SystemException(FILE_IO_ERROR); + throw new FileIOException(FILE_IO_ERROR); } if (i < fields.length - 1) { writer.write(","); From 60055d0c6e0c81366ef7abf5fd1449a99e65e156 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:59:57 +0900 Subject: [PATCH 30/97] =?UTF-8?q?[change]=20=EC=BD=98=EC=86=94=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/IO/Console.java | 45 +++++-------------- .../IO/ConsoleValidatorConfig.java | 38 ++++++++++++++++ 2 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java index 15ca429a5d..a94003b92a 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java @@ -2,52 +2,31 @@ import com.programmers.springbootbasic.presentation.IOManager; import com.programmers.springbootbasic.presentation.validation.InputValidator; -import jakarta.annotation.PostConstruct; import java.util.Scanner; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component public class Console implements IOManager { - private final Scanner scanner = new Scanner(System.in); - private InputValidator stringValidator; - private InputValidator integerValidator; - @PostConstruct - private void setValidator() { - InputValidator stringValidator = new InputValidator<>(); - stringValidator.addValidator(input -> { - if(input == null || input.isEmpty()) { - return "입력 값이 없습니다."; - } - return null; - }); - this.stringValidator = stringValidator; + private final Scanner scanner; + private final InputValidator stringValidator; + private final InputValidator integerValidator; - InputValidator integerValidator = new InputValidator<>(); - integerValidator.addValidator(input -> { - if(input == null || input.isEmpty()) { - return "입력 값이 없습니다."; - } - return null; - }); - integerValidator.addValidator(input -> { - try { - Integer.parseInt(input); - return null; - } catch (NumberFormatException e) { - return "숫자가 아닙니다."; - } - }); + public Console(@Qualifier("getStringValidator") InputValidator stringValidator, + @Qualifier("getIntegerValidator") InputValidator integerValidator) { + this.scanner = new Scanner(System.in); + this.stringValidator = stringValidator; this.integerValidator = integerValidator; } public String collectStringInput(String message) { - while(true) { + while (true) { print(message); var result = scanner.nextLine().trim(); var errors = stringValidator.validate(result); - if(errors.isEmpty()) { + if (errors.isEmpty()) { return result; } errors.get().forEach(this::print); @@ -55,12 +34,12 @@ public String collectStringInput(String message) { } public int collectIntegerInput(String message) { - while(true) { + while (true) { print(message); var result = scanner.nextLine().trim(); var errors = integerValidator.validate(result); - if(errors.isEmpty()) { + if (errors.isEmpty()) { return Integer.parseInt(result); } errors.get().forEach(this::print); diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java new file mode 100644 index 0000000000..1ff3e2d83b --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java @@ -0,0 +1,38 @@ +package com.programmers.springbootbasic.infrastructure.IO; + +import com.programmers.springbootbasic.presentation.validation.InputValidator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ConsoleValidatorConfig { + @Bean + public InputValidator getStringValidator() { + InputValidator stringValidator = new InputValidator<>(); + stringValidator.addValidator(this::validateString); + return stringValidator; + } + @Bean + public InputValidator getIntegerValidator() { + InputValidator integerValidator = new InputValidator<>(); + integerValidator.addValidator(this::validateString); + integerValidator.addValidator(this::validateInteger); + return integerValidator; + } + + private String validateString(String input) { + if(input == null || input.isEmpty()) { + return "입력 값이 없습니다."; + } + return null; + } + private String validateInteger(String input) { + try { + Integer.parseInt(input); + return null; + } catch (NumberFormatException e) { + return "숫자가 아닙니다."; + } + } + +} From dd3bebd82c92776b195c58bd03a2ad72b35eb8ae Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:00:26 +0900 Subject: [PATCH 31/97] =?UTF-8?q?[change]=20=EC=96=B4=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=8B=A4=ED=96=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/VoucherApplication.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java index b1702ba586..5f0b6e637a 100644 --- a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java +++ b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java @@ -1,11 +1,8 @@ package com.programmers.springbootbasic; import com.programmers.springbootbasic.exception.AppExceptionHandler; -import com.programmers.springbootbasic.infrastructure.IO.Console; -import com.programmers.springbootbasic.mediator.RequestProcessor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication @@ -16,13 +13,14 @@ public static void main(String[] args) { VoucherApplication.class, args); + startAppWithErrorHandler(applicationContext); + } + + private static void startAppWithErrorHandler(ConfigurableApplicationContext applicationContext) { AppExceptionHandler exceptionHandler = applicationContext.getBean( AppExceptionHandler.class); - - // 컨텍스트가 종료된 경우 do { exceptionHandler.handle(); } while (applicationContext.isActive()); } - } From ac0e82cf294c3170a14f721c22c25ec0070d89b9 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:03:02 +0900 Subject: [PATCH 32/97] =?UTF-8?q?[change]=20=ED=8C=8C=EC=9D=BC=20IO=20?= =?UTF-8?q?=EB=8B=B4=EB=8B=B9=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/UserService.java | 6 ++-- .../domain/user/domain/entity/User.java | 14 ++------ .../user/infrastructure/dto/CsvUser.java | 34 +++++++++++++++++++ 3 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java index ea0fba1e37..89510f59c2 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java @@ -1,6 +1,6 @@ package com.programmers.springbootbasic.domain.user.application; -import com.programmers.springbootbasic.domain.user.domain.entity.User; +import com.programmers.springbootbasic.domain.user.infrastructure.dto.CsvUser; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; import com.programmers.springbootbasic.util.FileManager; import java.util.List; @@ -20,9 +20,9 @@ public UserService(FileManager fileManager, @Value("${file.user.path}") String f public List findBlacklistedUsers() { return fileManager - .read(fileName, User.class) + .read(fileName, CsvUser.class) .stream() - .map(UserResponse::of) + .map(csvUser -> UserResponse.of(csvUser.toEntity())) .toList(); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java index 4e551cc242..3c9e924cd7 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java @@ -1,17 +1,14 @@ package com.programmers.springbootbasic.domain.user.domain.entity; public class User { - private Long id; - private String name; + private final Long id; + private final String name; public User(Long id, String name) { this.id = id; this.name = name; } - public User() { - } - public Long getId() { return id; } @@ -20,11 +17,4 @@ public String getName() { return name; } - @Override - public String toString() { - return "User{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; - } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java new file mode 100644 index 0000000000..f2530ec530 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java @@ -0,0 +1,34 @@ +package com.programmers.springbootbasic.domain.user.infrastructure.dto; + +import com.programmers.springbootbasic.domain.user.domain.entity.User; + +public class CsvUser { + + private String id; + private String name; + + public CsvUser() { + } + + public CsvUser(String id, String name) { + this.id = id; + this.name = name; + } + + public static CsvUser of(User user) { + return new CsvUser(user.getId().toString(), user.getName()); + } + + public User toEntity() { + return new User(Long.valueOf(id), name); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + +} From 1f7e43d28bc47131d992fbbd8087242be097ed2a Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:12:47 +0900 Subject: [PATCH 33/97] =?UTF-8?q?[change]=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=EC=B2=B4=EC=A0=81=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/application/VoucherService.java | 4 ++-- .../domain/voucher/presentation/VoucherController.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java index 1168eb4353..f9abc76e0a 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java @@ -15,12 +15,12 @@ public VoucherService(VoucherRepository voucherRepository) { this.voucherRepository = voucherRepository; } - public void createVoucher(CreateVoucherRequest request) { + public void create(CreateVoucherRequest request) { // 중복 바우처 체크 voucherRepository.save(request.toEntity()); } - public List findAllVouchers() { + public List findAll() { return voucherRepository.findAll().stream() .map(VoucherResponse::of) .toList(); diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java index a48cc087cf..a49335208a 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java @@ -17,11 +17,11 @@ public VoucherController(VoucherService voucherService) { } public void createVoucher(CreateVoucherRequest request) { - voucherService.createVoucher(request); + voucherService.create(request); } public List getAllVouchers() { - return voucherService.findAllVouchers(); + return voucherService.findAll(); } } From a3e1cc9a3cb63a03e3fff950475d347fa9c888bf Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:29:03 +0900 Subject: [PATCH 34/97] =?UTF-8?q?[change]=20stream=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/validation/InputValidator.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java index c86cbfb470..45e0deab3e 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; public class InputValidator { @@ -10,12 +11,13 @@ public class InputValidator { @FunctionalInterface public interface validator { + String getErrorMessage(T val); } public Optional> validate(T input) { var errs = getError(input); - if(errs.isEmpty()) { + if (errs.isEmpty()) { return Optional.empty(); } return Optional.of(errs); @@ -26,13 +28,9 @@ public void addValidator(validator validator) { } private List getError(T input) { - List errorMessages = new ArrayList<>(); - for (validator validator : validators) { - String errorMessage = validator.getErrorMessage(input); - if (errorMessage != null) { - errorMessages.add(errorMessage); - } - } - return errorMessages; + return validators.stream() + .map(validator -> validator.getErrorMessage(input)) + .filter(Objects::nonNull) + .toList(); } } From 1680547741cfdb517c31401b429bdc2c83faf4ad Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:30:22 +0900 Subject: [PATCH 35/97] =?UTF-8?q?[change]=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=ED=8C=8C=EC=8A=A4=EC=B9=BC=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/validation/InputValidator.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java index 45e0deab3e..9259ad85ce 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java @@ -7,10 +7,10 @@ public class InputValidator { - protected final List> validators = new ArrayList<>(); + protected final List> validators = new ArrayList<>(); @FunctionalInterface - public interface validator { + public interface Validator { String getErrorMessage(T val); } @@ -23,7 +23,7 @@ public Optional> validate(T input) { return Optional.of(errs); } - public void addValidator(validator validator) { + public void addValidator(Validator validator) { this.validators.add(validator); } From 61141f018a6f239873c2e1a025f0115c13c397d3 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:34:46 +0900 Subject: [PATCH 36/97] =?UTF-8?q?[change]=20npe=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/validation/InputValidator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java index 9259ad85ce..87baf0d2a5 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/validation/InputValidator.java @@ -24,7 +24,9 @@ public Optional> validate(T input) { } public void addValidator(Validator validator) { - this.validators.add(validator); + if (validator != null) { + this.validators.add(validator); + } } private List getError(T input) { From 8bc2602ce23816cd7212ec3626a0ff8707ef5e8b Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:49:27 +0900 Subject: [PATCH 37/97] =?UTF-8?q?[change]=20=EC=9B=90=EC=8B=9C=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=9C=EB=84=A4=EB=A6=AD=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mediator/RequestProcessor.java | 7 +++---- .../requestGenerator/ExitRequestGenerator.java | 6 +++--- .../ViewAllBlackUserGenerator.java | 6 +++--- .../requestGenerator/ViewAllVoucherGenerator.java | 4 ++-- .../presentation/ControllerAdapter.java | 15 +++++++++------ .../springbootbasic/presentation/MainMenu.java | 4 ++-- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java index fc00832091..530a5a710e 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java @@ -42,9 +42,8 @@ public ConsoleResponse process(ConsoleRequest request) { } public void sendResponse(ConsoleResponse response) { - response.getBody().ifPresent((body) -> { - if (body instanceof List) { - List listBody = (List) body; + response.getBody().ifPresent(body -> { + if (body instanceof List listBody) { listBody.forEach( item -> consoleInteractionAggregator.displayMessage(item.toString())); } else { @@ -53,7 +52,7 @@ public void sendResponse(ConsoleResponse response) { }); response.getMessage().ifPresent( - (message) -> consoleInteractionAggregator.displayMessage( + message -> consoleInteractionAggregator.displayMessage( (String) message) ); diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java index aaf033b009..c0baf8d8e0 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java @@ -3,7 +3,7 @@ import com.programmers.springbootbasic.mediator.ConsoleRequest; import com.programmers.springbootbasic.presentation.MainMenu; -public class ExitRequestGenerator implements MenuRequestGenerator{ +public class ExitRequestGenerator implements MenuRequestGenerator { @Override public String getMenuCommand() { @@ -11,7 +11,7 @@ public String getMenuCommand() { } @Override - public ConsoleRequest generateRequest() { - return new ConsoleRequest(getMenuCommand()); + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand()); } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java index d294a3216f..7325375b38 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllBlackUserGenerator.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; @Component -public class ViewAllBlackUserGenerator implements MenuRequestGenerator{ +public class ViewAllBlackUserGenerator implements MenuRequestGenerator { @Override public String getMenuCommand() { @@ -13,7 +13,7 @@ public String getMenuCommand() { } @Override - public ConsoleRequest generateRequest() { - return new ConsoleRequest(getMenuCommand()); + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand()); } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java index 3caa171e95..0561003c17 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java @@ -13,7 +13,7 @@ public String getMenuCommand() { } @Override - public ConsoleRequest generateRequest() { - return new ConsoleRequest(getMenuCommand()); + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand()); } } diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java index ed64ae1f93..5c95a99196 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -6,10 +6,13 @@ import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_REGISTER; import com.programmers.springbootbasic.domain.user.presentation.UserController; +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; import com.programmers.springbootbasic.domain.voucher.presentation.VoucherController; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; import com.programmers.springbootbasic.exception.exceptionClass.SystemException; import com.programmers.springbootbasic.mediator.ConsoleResponse; +import java.util.List; import org.springframework.stereotype.Component; @Component @@ -23,24 +26,24 @@ public ControllerAdapter(VoucherController voucherController, UserController use this.userController = userController; } - public ConsoleResponse handleExit(Object... params) { + public ConsoleResponse handleExit(Object... params) { throw new SystemException(EXIT); } - public ConsoleResponse createVoucher(Object... params) { + public ConsoleResponse createVoucher(Object... params) { CreateVoucherRequest request = (CreateVoucherRequest) params[0]; voucherController.createVoucher(request); return new ConsoleResponse<>(SUCCESS_VOUCHER_REGISTER.getMessage()); } - public ConsoleResponse getAllVouchers(Object... params) { - return new ConsoleResponse(voucherController.getAllVouchers(), + public ConsoleResponse> getAllVouchers(Object... params) { + return new ConsoleResponse<>(voucherController.getAllVouchers(), SUCCESS_VOUCHER_LIST.getMessage() ); } - public ConsoleResponse getBlackList(Object... params) { - return new ConsoleResponse(userController.getBlackList(), + public ConsoleResponse> getBlackList(Object... params) { + return new ConsoleResponse<>(userController.getBlackList(), SUCCESS_BLACK_USER_LIST.getMessage()); } } diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index 8163618b39..cc331a1b5a 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -15,8 +15,8 @@ public enum MainMenu { LIST_BLACK_USER("list black user",ControllerAdapter::getBlackList), ; - private String command; - private BiFunction function; + private final String command; + private final BiFunction function; MainMenu(String command, BiFunction function) { this.command = command; From 7bb9b41011f6f8853957e8b98f44b6ba98fc1a48 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:10:39 +0900 Subject: [PATCH 38/97] =?UTF-8?q?[change]=20ConsoleResponse=20=EC=A0=95?= =?UTF-8?q?=EC=A0=81=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=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 --- .../exception/AppExceptionHandler.java | 12 ++++++------ .../springbootbasic/mediator/ConsoleResponse.java | 12 ++++++++++-- .../presentation/ControllerAdapter.java | 9 ++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java index f24cb64e75..8f9ae29e83 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java @@ -32,33 +32,33 @@ public void handle() { } catch (SystemException e) { logger.error(String.format("System Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + ConsoleResponse.createNoBodyResponse(e.getMessage()) ); handleExit(e); } catch (FileIOException e) { logger.error(String.format("FileIO Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + ConsoleResponse.createNoBodyResponse(e.getMessage()) ); } catch (CustomException e) { logger.error(String.format("Custom Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + ConsoleResponse.createNoBodyResponse(e.getMessage()) ); } catch (VoucherException e) { logger.error(String.format("Voucher Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + ConsoleResponse.createNoBodyResponse(e.getMessage()) ); } catch (UserException e) { logger.error(String.format("User Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + ConsoleResponse.createNoBodyResponse(e.getMessage()) ); } catch (Exception e) { logger.error(String.format("Unknown Error : %s", e.getMessage())); requestProcessor.sendResponse( - new ConsoleResponse(e.getMessage()) + ConsoleResponse.createNoBodyResponse(e.getMessage()) ); } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java index b61c0a33d0..41d89c40b6 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/ConsoleResponse.java @@ -7,11 +7,11 @@ public class ConsoleResponse { private final String message; private final T body; - public ConsoleResponse(String message) { + private ConsoleResponse(String message) { this(null, message); } - public ConsoleResponse(T body, String message) { + private ConsoleResponse(T body, String message) { this.body = body; this.message = message; } @@ -23,4 +23,12 @@ public Optional getMessage() { public Optional getBody() { return Optional.ofNullable(this.body); } + + public static ConsoleResponse createNoBodyResponse(String message) { + return new ConsoleResponse<>(message); + } + + public static ConsoleResponse createWithBodyResponse(T body, String message) { + return new ConsoleResponse<>(body, message); + } } diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java index 5c95a99196..a273f8ec6c 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -33,17 +33,16 @@ public ConsoleResponse handleExit(Object... params) { public ConsoleResponse createVoucher(Object... params) { CreateVoucherRequest request = (CreateVoucherRequest) params[0]; voucherController.createVoucher(request); - return new ConsoleResponse<>(SUCCESS_VOUCHER_REGISTER.getMessage()); + return ConsoleResponse.createNoBodyResponse(SUCCESS_VOUCHER_REGISTER.getMessage()); } public ConsoleResponse> getAllVouchers(Object... params) { - return new ConsoleResponse<>(voucherController.getAllVouchers(), - SUCCESS_VOUCHER_LIST.getMessage() - ); + return ConsoleResponse.createWithBodyResponse(voucherController.getAllVouchers(), + SUCCESS_VOUCHER_LIST.getMessage()); } public ConsoleResponse> getBlackList(Object... params) { - return new ConsoleResponse<>(userController.getBlackList(), + return ConsoleResponse.createWithBodyResponse(userController.getBlackList(), SUCCESS_BLACK_USER_LIST.getMessage()); } } From dcc4cfee9f4cf3d809edf110dc20f0859c4c79ae Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:13:42 +0900 Subject: [PATCH 39/97] =?UTF-8?q?[fix]=20jvm=20=EC=A2=85=EB=A3=8C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20+=20ExitRequestGenerator=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/mediator/RequestProcessor.java | 5 +---- .../mediator/requestGenerator/ExitRequestGenerator.java | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java index 530a5a710e..4c36651542 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java @@ -30,10 +30,7 @@ public void run() { public ConsoleRequest getRequest() { var menuInput = consoleInteractionAggregator.collectMenuInput(); - // 종료 처리는 어디서? - if (menuInput.equalsIgnoreCase("EXIT")) { - System.exit(0); - } + return menuRequestProvider.getMenuRequest(menuInput); } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java index c0baf8d8e0..5aea38933c 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ExitRequestGenerator.java @@ -2,7 +2,9 @@ import com.programmers.springbootbasic.mediator.ConsoleRequest; import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; +@Component public class ExitRequestGenerator implements MenuRequestGenerator { @Override From 0f715ef09bd3a29f3f6b316457b6c91872cfc421 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:40:35 +0900 Subject: [PATCH 40/97] =?UTF-8?q?[change]=20Voucher=20id=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/common/IdGenerator.java | 8 ++++++++ .../domain/voucher/application/VoucherService.java | 7 +++++-- .../voucher/domain/ProdVoucherIdGenerator.java | 13 +++++++++++++ .../domain/voucher/domain/VoucherIdGenerator.java | 7 +++++++ .../domain/voucher/domain/entity/Voucher.java | 4 ---- .../presentation/dto/CreateVoucherRequest.java | 5 +++-- 6 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/common/IdGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/ProdVoucherIdGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherIdGenerator.java diff --git a/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java b/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java new file mode 100644 index 0000000000..bd091e0779 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java @@ -0,0 +1,8 @@ +package com.programmers.springbootbasic.common; + +import java.util.UUID; + +public interface IdGenerator { + UUID generate(); + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java index f9abc76e0a..3b33905f6c 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java @@ -1,5 +1,6 @@ package com.programmers.springbootbasic.domain.voucher.application; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; @@ -10,14 +11,16 @@ public class VoucherService { private final VoucherRepository voucherRepository; + private final VoucherIdGenerator idGenerator; - public VoucherService(VoucherRepository voucherRepository) { + public VoucherService(VoucherRepository voucherRepository, VoucherIdGenerator idGenerator) { this.voucherRepository = voucherRepository; + this.idGenerator = idGenerator; } public void create(CreateVoucherRequest request) { // 중복 바우처 체크 - voucherRepository.save(request.toEntity()); + voucherRepository.save(request.toEntity(idGenerator.generate())); } public List findAll() { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/ProdVoucherIdGenerator.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/ProdVoucherIdGenerator.java new file mode 100644 index 0000000000..6835ee6f0e --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/ProdVoucherIdGenerator.java @@ -0,0 +1,13 @@ +package com.programmers.springbootbasic.domain.voucher.domain; + +import java.util.UUID; +import org.springframework.stereotype.Component; + +@Component +public class ProdVoucherIdGenerator implements VoucherIdGenerator { + + @Override + public UUID generate() { + return UUID.randomUUID(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherIdGenerator.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherIdGenerator.java new file mode 100644 index 0000000000..bed16108ae --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherIdGenerator.java @@ -0,0 +1,7 @@ +package com.programmers.springbootbasic.domain.voucher.domain; + +import com.programmers.springbootbasic.common.IdGenerator; + +public interface VoucherIdGenerator extends IdGenerator { + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java index c4019c77ba..d01f2898be 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java @@ -10,10 +10,6 @@ public class Voucher { private final VoucherType voucherType; private final Integer benefitValue; - public Voucher(VoucherType voucherType, Integer benefitValue) { - this(UUID.randomUUID(), voucherType, benefitValue); - } - public Voucher(UUID id, VoucherType voucherType, Integer benefitValue) { this.id = id; this.voucherType = voucherType; diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index c9934e00f8..5a0ffd4e15 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.UUID; public class CreateVoucherRequest { private final VoucherTypeEnum voucherTypeEnum; @@ -17,8 +18,8 @@ public static CreateVoucherRequest from(String voucherType, Integer benefitValue return new CreateVoucherRequest(voucherEnum, benefitValue); } - public Voucher toEntity() { - return new Voucher(voucherTypeEnum.getVoucherType(benefitValue), benefitValue); + public Voucher toEntity(UUID id) { + return new Voucher(id,voucherTypeEnum.getVoucherType(benefitValue), benefitValue); } public VoucherTypeEnum getVoucherType() { From 1afc46efdbc0e6afcf517d08437d9f7a60c3215e Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:48:26 +0900 Subject: [PATCH 41/97] [change] Reformat code --- .../springbootbasic/VoucherApplication.java | 4 +++- .../springbootbasic/common/IdGenerator.java | 1 + .../domain/user/domain/entity/User.java | 1 + .../domain/user/presentation/UserController.java | 1 + .../domain/user/presentation/dto/UserResponse.java | 1 + .../domain/voucher/domain/VoucherRepository.java | 4 ++-- .../domain/VoucherType/FixedAmountVoucher.java | 2 +- .../domain/VoucherType/PercentDiscountVoucher.java | 2 +- .../voucher/domain/VoucherType/VoucherType.java | 2 ++ .../domain/VoucherType/VoucherTypeEnum.java | 4 ++-- .../FilePersistenceVoucherRepository.java | 6 ++++-- .../voucher/presentation/VoucherController.java | 1 - .../presentation/dto/CreateVoucherRequest.java | 3 ++- .../exception/AppExceptionHandler.java | 7 ++++--- .../exception/exceptionClass/FileIOException.java | 8 ++++---- .../exception/exceptionClass/SystemException.java | 1 + .../exception/exceptionClass/UserException.java | 3 ++- .../exception/exceptionClass/VoucherException.java | 3 ++- .../springbootbasic/infrastructure/IO/Console.java | 6 ++++-- .../IO/ConsoleInteractionAggregator.java | 3 ++- .../infrastructure/IO/ConsoleValidatorConfig.java | 5 ++++- .../springbootbasic/mediator/RequestProcessor.java | 6 ++++-- .../requestGenerator/MenuRequestGenerator.java | 1 + .../requestGenerator/RegisterVoucherGenerator.java | 5 +++-- .../requestGenerator/ViewAllVoucherGenerator.java | 2 +- .../springbootbasic/presentation/IOManager.java | 2 ++ .../springbootbasic/presentation/MainMenu.java | 14 ++++++++------ .../springbootbasic/util/FileManager.java | 1 + src/main/resources/logback-spring.xml | 3 ++- 29 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java index 5f0b6e637a..98d657f55e 100644 --- a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java +++ b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java @@ -16,7 +16,9 @@ public static void main(String[] args) { startAppWithErrorHandler(applicationContext); } - private static void startAppWithErrorHandler(ConfigurableApplicationContext applicationContext) { + private static void startAppWithErrorHandler( + ConfigurableApplicationContext applicationContext + ) { AppExceptionHandler exceptionHandler = applicationContext.getBean( AppExceptionHandler.class); do { diff --git a/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java b/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java index bd091e0779..3c2ccda736 100644 --- a/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/common/IdGenerator.java @@ -3,6 +3,7 @@ import java.util.UUID; public interface IdGenerator { + UUID generate(); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java index 3c9e924cd7..ccb0ec42f0 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java @@ -1,6 +1,7 @@ package com.programmers.springbootbasic.domain.user.domain.entity; public class User { + private final Long id; private final String name; diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java index a99375b08c..0f84c659bc 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java @@ -7,6 +7,7 @@ @Controller public class UserController { + private final UserService userService; public UserController(UserService userService) { diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java index 148b6fb7db..2dc3e75c42 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java @@ -3,6 +3,7 @@ import com.programmers.springbootbasic.domain.user.domain.entity.User; public class UserResponse { + private final String name; public UserResponse(String name) { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java index fd4a484612..89a4195bb0 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java @@ -2,10 +2,10 @@ import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import java.util.List; -import java.util.Optional; -import java.util.UUID; public interface VoucherRepository { + Voucher save(Voucher voucher); + List findAll(); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java index 16c63ce0f1..fe76a58a8c 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java @@ -5,7 +5,7 @@ import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; -public class FixedAmountVoucher implements VoucherType{ +public class FixedAmountVoucher implements VoucherType { public FixedAmountVoucher(Integer benefit) { if (benefit < 0) { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java index 6f9512e05d..09af9d0836 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java @@ -5,7 +5,7 @@ import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; -public class PercentDiscountVoucher implements VoucherType{ +public class PercentDiscountVoucher implements VoucherType { public PercentDiscountVoucher(Integer benefit) { if (benefit < 0 || benefit > 100) { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java index 52b46c70c9..c2afa349c4 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherType.java @@ -1,6 +1,8 @@ package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; public interface VoucherType { + String getVoucherTypeName(); + double getDiscountedPrice(double price, int benefitValue); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java index c78a232436..418b53fb92 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java @@ -9,9 +9,9 @@ public enum VoucherTypeEnum { FIXED(FixedAmountVoucher::new), PERCENT(PercentDiscountVoucher::new); - private final Function voucherType; + private final Function voucherType; - VoucherTypeEnum(Function voucherType) { + VoucherTypeEnum(Function voucherType) { this.voucherType = voucherType; } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java index 33532994e4..2e87d2356e 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java @@ -20,8 +20,10 @@ public class FilePersistenceVoucherRepository implements VoucherRepository { private FileManager fileManager; private final String fileName; - public FilePersistenceVoucherRepository(List fileManagerList, - @Value("${file.voucher.path}") String fileName) { + public FilePersistenceVoucherRepository( + List fileManagerList, + @Value("${file.voucher.path}") String fileName + ) { this.fileName = fileName; fileManagerList.stream().filter(fm -> fm.supports(fileName)) .findFirst() diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java index a49335208a..c5aeb1aaa7 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java @@ -1,7 +1,6 @@ package com.programmers.springbootbasic.domain.voucher.presentation; import com.programmers.springbootbasic.domain.voucher.application.VoucherService; -import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; import java.util.List; diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index 5a0ffd4e15..79ba57dbc5 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -5,6 +5,7 @@ import java.util.UUID; public class CreateVoucherRequest { + private final VoucherTypeEnum voucherTypeEnum; private final Integer benefitValue; @@ -19,7 +20,7 @@ public static CreateVoucherRequest from(String voucherType, Integer benefitValue } public Voucher toEntity(UUID id) { - return new Voucher(id,voucherTypeEnum.getVoucherType(benefitValue), benefitValue); + return new Voucher(id, voucherTypeEnum.getVoucherType(benefitValue), benefitValue); } public VoucherTypeEnum getVoucherType() { diff --git a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java index 8f9ae29e83..34b41f77cd 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java @@ -9,7 +9,6 @@ import com.programmers.springbootbasic.mediator.RequestProcessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Component; @@ -20,8 +19,10 @@ public class AppExceptionHandler { private final RequestProcessor requestProcessor; private final ConfigurableApplicationContext context; - public AppExceptionHandler(RequestProcessor requestProcessor, - ConfigurableApplicationContext context) { + public AppExceptionHandler( + RequestProcessor requestProcessor, + ConfigurableApplicationContext context + ) { this.requestProcessor = requestProcessor; this.context = context; } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java index 4cb3e6619a..45ca49deb0 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java @@ -2,9 +2,9 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class FileIOException extends RuntimeException{ +public class FileIOException extends RuntimeException { - public FileIOException(ErrorCode errorCode) { - super(errorCode.getMessage()); - } + public FileIOException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java index 9a582d0a99..7477588421 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java @@ -3,6 +3,7 @@ import com.programmers.springbootbasic.exception.ErrorCode; public class SystemException extends RuntimeException { + private final ErrorCode errorCode; public SystemException(ErrorCode errorCode) { diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java index a7c1990064..854535ca42 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java @@ -2,7 +2,8 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class UserException extends RuntimeException{ +public class UserException extends RuntimeException { + UserException(ErrorCode errorCode) { super(errorCode.getMessage()); } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java index d97a38139c..e43519efef 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java @@ -2,7 +2,8 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class VoucherException extends RuntimeException{ +public class VoucherException extends RuntimeException { + public VoucherException(ErrorCode errorCode) { super(errorCode.getMessage()); } diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java index a94003b92a..3475206877 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java @@ -13,8 +13,10 @@ public class Console implements IOManager { private final InputValidator stringValidator; private final InputValidator integerValidator; - public Console(@Qualifier("getStringValidator") InputValidator stringValidator, - @Qualifier("getIntegerValidator") InputValidator integerValidator) { + public Console( + @Qualifier("getStringValidator") InputValidator stringValidator, + @Qualifier("getIntegerValidator") InputValidator integerValidator + ) { this.scanner = new Scanner(System.in); this.stringValidator = stringValidator; this.integerValidator = integerValidator; diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java index b41dc74e65..5fb23d3a1a 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java @@ -5,7 +5,8 @@ import org.springframework.stereotype.Component; @Component -public class ConsoleInteractionAggregator{ +public class ConsoleInteractionAggregator { + private final Console console; public ConsoleInteractionAggregator(Console console) { diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java index 1ff3e2d83b..e2ae42520b 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java @@ -6,12 +6,14 @@ @Configuration public class ConsoleValidatorConfig { + @Bean public InputValidator getStringValidator() { InputValidator stringValidator = new InputValidator<>(); stringValidator.addValidator(this::validateString); return stringValidator; } + @Bean public InputValidator getIntegerValidator() { InputValidator integerValidator = new InputValidator<>(); @@ -21,11 +23,12 @@ public InputValidator getIntegerValidator() { } private String validateString(String input) { - if(input == null || input.isEmpty()) { + if (input == null || input.isEmpty()) { return "입력 값이 없습니다."; } return null; } + private String validateInteger(String input) { try { Integer.parseInt(input); diff --git a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java index 4c36651542..6a768ffa81 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/RequestProcessor.java @@ -14,9 +14,11 @@ public class RequestProcessor { private final MenuRequestProvider menuRequestProvider; private final ConsoleInteractionAggregator consoleInteractionAggregator; - public RequestProcessor(ControllerAdapter controllerAdapter, + public RequestProcessor( + ControllerAdapter controllerAdapter, MenuRequestProvider menuRequestProvider, - ConsoleInteractionAggregator consoleInteractionAggregator) { + ConsoleInteractionAggregator consoleInteractionAggregator + ) { this.controllerAdapter = controllerAdapter; this.menuRequestProvider = menuRequestProvider; this.consoleInteractionAggregator = consoleInteractionAggregator; diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java index f228164a38..b920367182 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/MenuRequestGenerator.java @@ -3,6 +3,7 @@ import com.programmers.springbootbasic.mediator.ConsoleRequest; public interface MenuRequestGenerator { + String getMenuCommand(); ConsoleRequest generateRequest(); diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java index 4be73e9737..7c5a62e59d 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterVoucherGenerator.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Component; @Component -public class RegisterVoucherGenerator implements MenuRequestGenerator{ +public class RegisterVoucherGenerator implements MenuRequestGenerator { private final ConsoleInteractionAggregator consoleInteractionAggregator; @@ -22,6 +22,7 @@ public String getMenuCommand() { @Override public ConsoleRequest generateRequest() { - return new ConsoleRequest<>(getMenuCommand(),consoleInteractionAggregator.collectVoucherInput()); + return new ConsoleRequest<>(getMenuCommand(), + consoleInteractionAggregator.collectVoucherInput()); } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java index 0561003c17..51cb5e7e8d 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; @Component -public class ViewAllVoucherGenerator implements MenuRequestGenerator{ +public class ViewAllVoucherGenerator implements MenuRequestGenerator { @Override public String getMenuCommand() { diff --git a/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java b/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java index b584f43718..9c8c835d43 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/IOManager.java @@ -1,6 +1,8 @@ package com.programmers.springbootbasic.presentation; public interface IOManager { + String collectStringInput(String message); + void print(String message); } diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index cc331a1b5a..2a60710914 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -9,10 +9,10 @@ import java.util.stream.Stream; public enum MainMenu { - EXIT("exit",ControllerAdapter::handleExit), - CREATE_VOUCHER("create voucher",ControllerAdapter::createVoucher), - LIST_VOUCHER("list voucher",ControllerAdapter::getAllVouchers), - LIST_BLACK_USER("list black user",ControllerAdapter::getBlackList), + EXIT("exit", ControllerAdapter::handleExit), + CREATE_VOUCHER("create voucher", ControllerAdapter::createVoucher), + LIST_VOUCHER("list voucher", ControllerAdapter::getAllVouchers), + LIST_BLACK_USER("list black user", ControllerAdapter::getBlackList), ; private final String command; @@ -31,8 +31,10 @@ public ConsoleResponse execute(ControllerAdapter controller, Object... params) { return function.apply(controller, params); } - public static ConsoleResponse routeToController(ConsoleRequest req, - ControllerAdapter controllerAdapter){ + public static ConsoleResponse routeToController( + ConsoleRequest req, + ControllerAdapter controllerAdapter + ) { return Stream.of(values()) .filter(menuCommand -> menuCommand.getCommand().equals(req.getCommand())) .findFirst() diff --git a/src/main/java/com/programmers/springbootbasic/util/FileManager.java b/src/main/java/com/programmers/springbootbasic/util/FileManager.java index 2a02126120..61a9141c35 100644 --- a/src/main/java/com/programmers/springbootbasic/util/FileManager.java +++ b/src/main/java/com/programmers/springbootbasic/util/FileManager.java @@ -3,6 +3,7 @@ import java.util.List; public interface FileManager { + boolean supports(String fileName); List read(String fileName, Class type); diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 6c11b15e1e..8b4915e088 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -5,7 +5,8 @@ - + From 8fa37df867b5954a8becd0f514f8b65939029813 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 26 Oct 2023 03:00:35 +0900 Subject: [PATCH 42/97] =?UTF-8?q?[change]=20VoucherRepository=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=B3=80=ED=99=94?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voucher/domain/VoucherRepository.java | 8 ++++ .../FilePersistenceVoucherRepository.java | 48 ++++++++++++++++--- .../MemoryVoucherRepository.java | 16 +++++++ .../infrastructure/{ => dto}/CsvVoucher.java | 2 +- .../springbootbasic/util/CsvManager.java | 4 +- .../springbootbasic/util/FileManager.java | 2 +- 6 files changed, 69 insertions(+), 11 deletions(-) rename src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/{ => dto}/CsvVoucher.java (99%) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java index 89a4195bb0..b0ff85f080 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java @@ -2,10 +2,18 @@ import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import java.util.List; +import java.util.Optional; +import java.util.UUID; public interface VoucherRepository { Voucher save(Voucher voucher); List findAll(); + + Optional findById(UUID id); + + int deleteById(UUID id); + + int update(Voucher voucher); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java index 2e87d2356e..a8262edd22 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java @@ -4,9 +4,13 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.CsvVoucher; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; import com.programmers.springbootbasic.util.FileManager; import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; @@ -17,24 +21,23 @@ @Primary public class FilePersistenceVoucherRepository implements VoucherRepository { - private FileManager fileManager; + private final FileManager fileManager; private final String fileName; + private final ConcurrentHashMap vouchers; public FilePersistenceVoucherRepository( List fileManagerList, @Value("${file.voucher.path}") String fileName + ) { this.fileName = fileName; - fileManagerList.stream().filter(fm -> fm.supports(fileName)) - .findFirst() - .ifPresentOrElse(fm -> this.fileManager = fm, () -> { - throw new VoucherException(INVALID_FILE_PATH); - }); + this.fileManager = initializeFileManager(fileManagerList, fileName); + this.vouchers = getCsvVoucherHashMap(); } @Override public Voucher save(Voucher voucher) { - fileManager.write(CsvVoucher.of(voucher), fileName); + fileManager.write(CsvVoucher.of(voucher), fileName, true); return voucher; } @@ -45,4 +48,35 @@ public List findAll() { .map(CsvVoucher::toEntity) .toList(); } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(vouchers.get(id)); + } + + @Override + public int deleteById(UUID id) { + return vouchers.remove(id) == null ? 0 : 1; + } + + @Override + public int update(Voucher voucher) { + return vouchers.put(voucher.getId(), voucher) == null ? 0 : 1; + } + + private FileManager initializeFileManager(List fileManagerList, String fileName) { + return fileManagerList.stream() + .filter(fm -> fm.supports(fileName)) + .findFirst() + .orElseThrow(() -> new VoucherException(INVALID_FILE_PATH)); + } + + private ConcurrentHashMap getCsvVoucherHashMap() { + var csvVouchers = fileManager.read(fileName, CsvVoucher.class); + ConcurrentHashMap csvVoucherHashMap = new ConcurrentHashMap<>(); + csvVouchers.forEach( + csvVoucher -> csvVoucherHashMap.put(UUID.fromString(csvVoucher.getId()), + csvVoucher.toEntity())); + return csvVoucherHashMap; + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java index 7b55ca53b8..98f113a9e1 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java @@ -3,6 +3,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Repository; @@ -22,4 +23,19 @@ public Voucher save(Voucher voucher) { public List findAll() { return vouchers.values().stream().toList(); } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(vouchers.get(id)); + } + + @Override + public int deleteById(UUID id) { + return vouchers.remove(id) == null ? 0 : 1; + } + + @Override + public int update(Voucher voucher) { + return vouchers.put(voucher.getId(), voucher) == null ? 0 : 1; + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java similarity index 99% rename from src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java rename to src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java index 00cf38d837..fd6289f6fa 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/CsvVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java @@ -1,4 +1,4 @@ -package com.programmers.springbootbasic.domain.voucher.infrastructure; +package com.programmers.springbootbasic.domain.voucher.infrastructure.dto; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; diff --git a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java index ef1ccf3cdd..0dcb11889a 100644 --- a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java +++ b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java @@ -54,9 +54,9 @@ public List read(String fileName, Class type) { } @Override - public void write(T entity, String fileName) { + public void write(T entity, String fileName, boolean append) { File file = getFileResource(fileName); - try (BufferedWriter writer = new BufferedWriter(new FileWriter(file, true))) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file, append))) { if (file.length() == 0) { writeHeaders(writer, entity); } diff --git a/src/main/java/com/programmers/springbootbasic/util/FileManager.java b/src/main/java/com/programmers/springbootbasic/util/FileManager.java index 61a9141c35..a2f5dabea8 100644 --- a/src/main/java/com/programmers/springbootbasic/util/FileManager.java +++ b/src/main/java/com/programmers/springbootbasic/util/FileManager.java @@ -8,5 +8,5 @@ public interface FileManager { List read(String fileName, Class type); - void write(T entity, String fileName); + void write(T entity, String fileName, boolean append); } From 249e94a70ce0027e917e4ed1add831054f47dedd Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:21:19 +0900 Subject: [PATCH 43/97] =?UTF-8?q?[change]=20CommonRepository=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/common/Repository.java | 17 +++++++++++++++++ .../voucher/domain/VoucherRepository.java | 14 ++------------ 2 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/common/Repository.java diff --git a/src/main/java/com/programmers/springbootbasic/common/Repository.java b/src/main/java/com/programmers/springbootbasic/common/Repository.java new file mode 100644 index 0000000000..b74906fdde --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/Repository.java @@ -0,0 +1,17 @@ +package com.programmers.springbootbasic.common; + +import java.util.List; +import java.util.Optional; + +public interface Repository { + + S save(S entity); + + Optional findById(ID id); + + List findAll(); + + int deleteById(ID id); + + int update(S entity); +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java index b0ff85f080..8a4e7153c3 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java @@ -1,19 +1,9 @@ package com.programmers.springbootbasic.domain.voucher.domain; +import com.programmers.springbootbasic.common.Repository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; -import java.util.List; -import java.util.Optional; import java.util.UUID; -public interface VoucherRepository { +public interface VoucherRepository extends Repository { - Voucher save(Voucher voucher); - - List findAll(); - - Optional findById(UUID id); - - int deleteById(UUID id); - - int update(Voucher voucher); } From 75b1c39cb68f06627a476e40825a8806acfc1abd Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:06:33 +0900 Subject: [PATCH 44/97] =?UTF-8?q?[change]=20=EC=9C=A0=EC=A0=80=20name=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20nickname=20=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/domain/entity/User.java | 10 +++++----- .../domain/user/infrastructure/dto/CsvUser.java | 2 +- .../domain/user/presentation/dto/UserResponse.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java index ccb0ec42f0..1a01f246ce 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java @@ -3,19 +3,19 @@ public class User { private final Long id; - private final String name; + private final String nickname; - public User(Long id, String name) { + public User(Long id, String nickname) { this.id = id; - this.name = name; + this.nickname = nickname; } public Long getId() { return id; } - public String getName() { - return name; + public String getNickname() { + return nickname; } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java index f2530ec530..3e7b2af373 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java @@ -16,7 +16,7 @@ public CsvUser(String id, String name) { } public static CsvUser of(User user) { - return new CsvUser(user.getId().toString(), user.getName()); + return new CsvUser(user.getId().toString(), user.getNickname()); } public User toEntity() { diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java index 2dc3e75c42..10603e02eb 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java @@ -11,7 +11,7 @@ public UserResponse(String name) { } public static UserResponse of(User user) { - return new UserResponse(user.getName()); + return new UserResponse(user.getNickname()); } @Override From d0cc911df4a671af4ff6fd8acc9c24b50dbad130 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:25:44 +0900 Subject: [PATCH 45/97] =?UTF-8?q?[feat]=20=EC=9C=A0=EC=A0=80=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20jdbc=20=EC=A0=81=EC=9A=A9=ED=95=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/UserService.java | 21 +++-- .../domain/user/domain/UserRepository.java | 13 +++ .../domain/user/domain/entity/User.java | 27 +++++- .../infrastructure/JdbcUserRepository.java | 89 +++++++++++++++++++ .../user/infrastructure/dto/CsvUser.java | 9 +- .../user/presentation/dto/UserResponse.java | 25 +++++- .../exceptionClass/UserException.java | 2 +- 7 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/domain/UserRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepository.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java index 89510f59c2..feaa48264c 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java @@ -1,28 +1,27 @@ package com.programmers.springbootbasic.domain.user.application; -import com.programmers.springbootbasic.domain.user.infrastructure.dto.CsvUser; +import com.programmers.springbootbasic.domain.user.domain.UserRepository; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; -import com.programmers.springbootbasic.util.FileManager; import java.util.List; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) public class UserService { - private final FileManager fileManager; - private final String fileName; + private final UserRepository userRepository; - public UserService(FileManager fileManager, @Value("${file.user.path}") String fileName) { - this.fileName = fileName; - this.fileManager = fileManager; + public UserService( + UserRepository userRepository + ) { + this.userRepository = userRepository; } public List findBlacklistedUsers() { - return fileManager - .read(fileName, CsvUser.class) + return userRepository.findBlacklistedUsers() .stream() - .map(csvUser -> UserResponse.of(csvUser.toEntity())) + .map(UserResponse::of) .toList(); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/domain/UserRepository.java b/src/main/java/com/programmers/springbootbasic/domain/user/domain/UserRepository.java new file mode 100644 index 0000000000..64815052d2 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/domain/UserRepository.java @@ -0,0 +1,13 @@ +package com.programmers.springbootbasic.domain.user.domain; + +import com.programmers.springbootbasic.common.Repository; +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import java.util.List; +import java.util.Optional; + +public interface UserRepository extends Repository { + + List findBlacklistedUsers(); + + Optional findByNickname(String nickname); +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java index 1a01f246ce..2183849446 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/domain/entity/User.java @@ -1,13 +1,17 @@ package com.programmers.springbootbasic.domain.user.domain.entity; +import java.util.Objects; + public class User { private final Long id; private final String nickname; + private final boolean blocked; - public User(Long id, String nickname) { + public User(Long id, String nickname, boolean blocked) { this.id = id; this.nickname = nickname; + this.blocked = blocked; } public Long getId() { @@ -18,4 +22,25 @@ public String getNickname() { return nickname; } + public boolean isBlocked() { + return blocked; + } + + // distinct()를 위해 equals와 hashCode를 재정의 해야 한다. + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof User user)) { + return false; + } + return isBlocked() == user.isBlocked() && Objects.equals(getId(), user.getId()) + && Objects.equals(getNickname(), user.getNickname()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getNickname(), isBlocked()); + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepository.java b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepository.java new file mode 100644 index 0000000000..7d21444aeb --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepository.java @@ -0,0 +1,89 @@ +package com.programmers.springbootbasic.domain.user.infrastructure; + +import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import java.sql.PreparedStatement; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +@Repository +public class JdbcUserRepository implements UserRepository { + + private static final String TABLE_NAME = "users"; + private static final String ID = "id"; + private static final String NICKNAME = "nickname"; + private static final String BLOCKED = "blocked"; + + private final JdbcTemplate jdbcTemplate; + + public JdbcUserRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + private final RowMapper userRowMapper = + (rs, rowNum) -> new User(rs.getLong(ID), rs.getString(NICKNAME), rs.getBoolean(BLOCKED)); + + @Override + public User save(User entity) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection + .prepareStatement( + String.format("INSERT INTO %s (%s, %s) VALUES (?, ?)", TABLE_NAME, NICKNAME, + BLOCKED), + new String[]{ID}); + ps.setString(1, entity.getNickname()); + ps.setBoolean(2, entity.isBlocked()); + return ps; + }, keyHolder); + + // key 없으면 npe + return new User(Objects.requireNonNull(keyHolder.getKey()).longValue(), + entity.getNickname(), entity.isBlocked()); + } + + @Override + public Optional findById(Long id) { + List users = jdbcTemplate.query( + String.format("SELECT * FROM %s WHERE %s = ?", TABLE_NAME, ID), userRowMapper, id); + return users.stream().findFirst(); + } + + @Override + public List findAll() { + return jdbcTemplate.query(String.format("SELECT * FROM %s", TABLE_NAME), userRowMapper); + } + + @Override + public int deleteById(Long id) { + return jdbcTemplate.update(String.format("DELETE FROM %s WHERE %s = ?", TABLE_NAME, ID), + id); + } + + @Override + public int update(User entity) { + return jdbcTemplate.update( + String.format("UPDATE %s SET %s = ?, %s = ? WHERE %s = ?", TABLE_NAME, NICKNAME, + BLOCKED, ID), + entity.getNickname(), entity.isBlocked(), entity.getId()); + } + + @Override + public List findBlacklistedUsers() { + return jdbcTemplate.query( + String.format("SELECT * FROM %s WHERE %s = true", TABLE_NAME, BLOCKED), userRowMapper); + } + + @Override + public Optional findByNickname(String nickname) { + return jdbcTemplate.query( + String.format("SELECT * FROM %s WHERE %s = ?", TABLE_NAME, NICKNAME), userRowMapper, + nickname).stream().findFirst(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java index 3e7b2af373..00cb0fd8ce 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/infrastructure/dto/CsvUser.java @@ -6,21 +6,24 @@ public class CsvUser { private String id; private String name; + private String blocked; public CsvUser() { } - public CsvUser(String id, String name) { + public CsvUser(String id, String name, String blocked) { this.id = id; this.name = name; + this.blocked = blocked; } public static CsvUser of(User user) { - return new CsvUser(user.getId().toString(), user.getNickname()); + return new CsvUser(user.getId().toString(), user.getNickname(), + String.valueOf(user.isBlocked())); } public User toEntity() { - return new User(Long.valueOf(id), name); + return new User(Long.valueOf(id), name, Boolean.getBoolean(blocked)); } public String getId() { diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java index 10603e02eb..1817978cb9 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java @@ -1,13 +1,14 @@ package com.programmers.springbootbasic.domain.user.presentation.dto; import com.programmers.springbootbasic.domain.user.domain.entity.User; +import java.util.Objects; public class UserResponse { - private final String name; + private final String nickname; - public UserResponse(String name) { - this.name = name; + public UserResponse(String nickname) { + this.nickname = nickname; } public static UserResponse of(User user) { @@ -19,6 +20,22 @@ public String toString() { return """ name = %s =============================== - """.formatted(name); + """.formatted(nickname); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof UserResponse that)) { + return false; + } + return Objects.equals(nickname, that.nickname); + } + + @Override + public int hashCode() { + return Objects.hash(nickname); } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java index 854535ca42..5cd5a34215 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java @@ -4,7 +4,7 @@ public class UserException extends RuntimeException { - UserException(ErrorCode errorCode) { + public UserException(ErrorCode errorCode) { super(errorCode.getMessage()); } } From ee8f67d78a28e26ac29e3b70691ecd7a977d8f3d Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:26:51 +0900 Subject: [PATCH 46/97] =?UTF-8?q?[add]=20common=20class=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/common/BaseEntity.java | 33 +++++++++++++++++++ .../springbootbasic/common/Repository.java | 6 ++-- 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/common/BaseEntity.java diff --git a/src/main/java/com/programmers/springbootbasic/common/BaseEntity.java b/src/main/java/com/programmers/springbootbasic/common/BaseEntity.java new file mode 100644 index 0000000000..fb11fbb7ff --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/BaseEntity.java @@ -0,0 +1,33 @@ +package com.programmers.springbootbasic.common; + +import java.time.LocalDateTime; +import java.util.Objects; + +public abstract class BaseEntity { + + private final LocalDateTime createdAt; + + protected BaseEntity(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BaseEntity that)) { + return false; + } + return Objects.equals(getCreatedAt(), that.getCreatedAt()); + } + + @Override + public int hashCode() { + return Objects.hash(getCreatedAt()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/Repository.java b/src/main/java/com/programmers/springbootbasic/common/Repository.java index b74906fdde..ea6f7b0e9c 100644 --- a/src/main/java/com/programmers/springbootbasic/common/Repository.java +++ b/src/main/java/com/programmers/springbootbasic/common/Repository.java @@ -3,9 +3,9 @@ import java.util.List; import java.util.Optional; -public interface Repository { +public interface Repository { - S save(S entity); + T save(T entity); Optional findById(ID id); @@ -13,5 +13,5 @@ public interface Repository { int deleteById(ID id); - int update(S entity); + int update(T entity); } From e5d267e6d9b5a81a0e0c83b1fd4321f0de59c01f Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:28:03 +0900 Subject: [PATCH 47/97] =?UTF-8?q?[fix]=20voucher=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=200=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/domain/VoucherType/FixedAmountVoucher.java | 4 +++- .../voucher/domain/VoucherType/PercentDiscountVoucher.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java index fe76a58a8c..43a25d5dd1 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucher.java @@ -4,11 +4,13 @@ import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_FIXED_VOUCHER_BENEFIT; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode public class FixedAmountVoucher implements VoucherType { public FixedAmountVoucher(Integer benefit) { - if (benefit < 0) { + if (benefit <= 0) { throw new VoucherException(INVALID_FIXED_VOUCHER_BENEFIT); } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java index 09af9d0836..24cf5f62bb 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucher.java @@ -4,11 +4,13 @@ import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_PERCENT_VOUCHER_BENEFIT; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import lombok.EqualsAndHashCode; +@EqualsAndHashCode public class PercentDiscountVoucher implements VoucherType { public PercentDiscountVoucher(Integer benefit) { - if (benefit < 0 || benefit > 100) { + if (benefit <= 0 || benefit > 100) { throw new VoucherException(INVALID_PERCENT_VOUCHER_BENEFIT); } } From c7866e95448294e5c5d7327d881c2e5f45299c3c Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:30:44 +0900 Subject: [PATCH 48/97] =?UTF-8?q?[feat]=20voucher=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20jdbc=20=EC=A0=81=EC=9A=A9=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voucher/application/VoucherService.java | 52 ++++++++++++-- .../domain/voucher/domain/entity/Voucher.java | 19 +++++ .../infrastructure/JdbcVoucherRepository.java | 72 +++++++++++++++++++ .../presentation/VoucherController.java | 14 ++++ .../dto/CreateVoucherRequest.java | 20 +++--- .../dto/UpdateVoucherRequest.java | 24 +++++++ .../presentation/dto/VoucherResponse.java | 11 +++ .../dto/UpdateVoucherMediatorRequest.java | 42 +++++++++++ .../requestGenerator/DeleteMyVoucher.java | 27 +++++++ .../DeleteVoucherGenerator.java | 28 ++++++++ .../UpdateVoucherGenerator.java | 28 ++++++++ .../ViewAllVoucherGenerator.java | 2 +- .../presentation/ControllerAdapter.java | 20 ++++++ .../presentation/MainMenu.java | 5 +- .../springbootbasic/util/Messages.java | 17 +++++ 15 files changed, 366 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/dto/UpdateVoucherMediatorRequest.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteMyVoucher.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteVoucherGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/UpdateVoucherGenerator.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java index 3b33905f6c..ac9ad90f5b 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java @@ -1,26 +1,42 @@ package com.programmers.springbootbasic.domain.voucher.application; +import static com.programmers.springbootbasic.exception.ErrorCode.FAIL_TO_DELETE_VOUCHER; +import static com.programmers.springbootbasic.exception.ErrorCode.FAIL_TO_UPDATE_VOUCHER; +import static com.programmers.springbootbasic.exception.ErrorCode.NOT_FOUND_VOUCHER; + import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; import java.util.List; +import java.util.UUID; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) public class VoucherService { + private static final int AFFECTED_ROW_ONE = 1; private final VoucherRepository voucherRepository; private final VoucherIdGenerator idGenerator; - public VoucherService(VoucherRepository voucherRepository, VoucherIdGenerator idGenerator) { + public VoucherService( + VoucherRepository voucherRepository, + VoucherIdGenerator idGenerator + ) { this.voucherRepository = voucherRepository; this.idGenerator = idGenerator; } - public void create(CreateVoucherRequest request) { - // 중복 바우처 체크 - voucherRepository.save(request.toEntity(idGenerator.generate())); + @Transactional + public UUID create(CreateVoucherRequest request) { + Voucher voucher = request.toEntity(idGenerator.generate()); + voucherRepository.save(voucher); + return voucher.getId(); } public List findAll() { @@ -29,4 +45,32 @@ public List findAll() { .toList(); } + public VoucherResponse findById(UUID id) { + return voucherRepository.findById(id) + .map(VoucherResponse::of) + .orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); + } + + @Transactional + public void deleteById(UUID id) { + voucherRepository.findById(id).orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); + + if (voucherRepository.deleteById(id) != AFFECTED_ROW_ONE) { + throw new VoucherException(FAIL_TO_DELETE_VOUCHER); + } + } + + @Transactional + public UUID update(UUID id, UpdateVoucherRequest request) { + Voucher exsistedVoucher = voucherRepository.findById(id) + .orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); + + var updatedVoucher = new Voucher(id, request.getVoucherType(), request.getBenefitValue()); + + if (voucherRepository.update(updatedVoucher) != AFFECTED_ROW_ONE) { + throw new VoucherException(FAIL_TO_UPDATE_VOUCHER); + } + return updatedVoucher.getId(); + } + } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java index d01f2898be..2ce021f11a 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; +import java.util.Objects; import java.util.UUID; public class Voucher { @@ -28,4 +29,22 @@ public Integer getBenefitValue() { return benefitValue; } + // 테스트코드를 위해 재정의 + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Voucher voucher)) { + return false; + } + return Objects.equals(getId(), voucher.getId()) && Objects.equals( + getVoucherType(), voucher.getVoucherType()) && Objects.equals(getBenefitValue(), + voucher.getBenefitValue()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getVoucherType(), getBenefitValue()); + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java new file mode 100644 index 0000000000..84a9988e02 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java @@ -0,0 +1,72 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +@Repository +public class JdbcVoucherRepository implements VoucherRepository { + + private static final String TABLE_NAME = "vouchers"; + private static final String ID = "id"; + private static final String VOUCHER_TYPE = "voucher_type"; + private static final String BENEFIT_VALUE = "benefit_value"; + + private final JdbcTemplate jdbcTemplate; + private final RowMapper voucherRowMapper = + (rs, rowNum) -> new Voucher( + UUID.fromString(rs.getString(ID)), + VoucherTypeEnum.of(rs.getString(VOUCHER_TYPE)) + .getVoucherType(rs.getInt(BENEFIT_VALUE)), + rs.getInt(BENEFIT_VALUE) + ); + + public JdbcVoucherRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Voucher save(Voucher voucher) { + jdbcTemplate.update( + String.format("INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)", TABLE_NAME, ID, + VOUCHER_TYPE, BENEFIT_VALUE), + voucher.getId().toString(), + voucher.getVoucherType().getVoucherTypeName(), + voucher.getBenefitValue()); + return voucher; + } + + @Override + public List findAll() { + return jdbcTemplate.query(String.format("SELECT * FROM %s", TABLE_NAME), voucherRowMapper); + } + + @Override + public Optional findById(UUID id) { + return jdbcTemplate.query(String.format("SELECT * FROM %s WHERE %s = ?", TABLE_NAME, ID), + voucherRowMapper, id.toString()) + .stream().findFirst(); + } + + @Override + public int deleteById(UUID id) { + return jdbcTemplate.update(String.format("DELETE FROM %s WHERE %s = ?", TABLE_NAME, ID), + id.toString()); + } + + @Override + public int update(Voucher voucher) { + return jdbcTemplate.update( + String.format("UPDATE %s SET %s = ?, %s = ? WHERE %s = ?", TABLE_NAME, VOUCHER_TYPE, + BENEFIT_VALUE, ID), + voucher.getVoucherType().getVoucherTypeName(), + voucher.getBenefitValue(), + voucher.getId().toString()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java index c5aeb1aaa7..6b655d823c 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java @@ -2,8 +2,10 @@ import com.programmers.springbootbasic.domain.voucher.application.VoucherService; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; import java.util.List; +import java.util.UUID; import org.springframework.stereotype.Controller; @Controller @@ -23,4 +25,16 @@ public List getAllVouchers() { return voucherService.findAll(); } + public VoucherResponse getVoucherById(UUID id) { + return voucherService.findById(id); + } + + public void deleteVoucherById(UUID id) { + voucherService.deleteById(id); + } + + public void updateVoucher(UUID id, UpdateVoucherRequest request) { + voucherService.update(id, request); + } + } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index 79ba57dbc5..556c3ef47e 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -1,30 +1,32 @@ package com.programmers.springbootbasic.domain.voucher.presentation.dto; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import java.util.UUID; public class CreateVoucherRequest { - private final VoucherTypeEnum voucherTypeEnum; + private final VoucherType voucherType; private final Integer benefitValue; - private CreateVoucherRequest(VoucherTypeEnum voucherTypeEnum, Integer benefitValue) { - this.voucherTypeEnum = voucherTypeEnum; + private CreateVoucherRequest(VoucherType voucherType, Integer benefitValue) { + this.voucherType = voucherType; this.benefitValue = benefitValue; } - public static CreateVoucherRequest from(String voucherType, Integer benefitValue) { - VoucherTypeEnum voucherEnum = VoucherTypeEnum.of(voucherType); - return new CreateVoucherRequest(voucherEnum, benefitValue); + public static CreateVoucherRequest of(String stringVoucherType, Integer benefitValue) { + VoucherTypeEnum voucherEnum = VoucherTypeEnum.of(stringVoucherType); + VoucherType voucherType = voucherEnum.getVoucherType(benefitValue); + return new CreateVoucherRequest(voucherType, benefitValue); } public Voucher toEntity(UUID id) { - return new Voucher(id, voucherTypeEnum.getVoucherType(benefitValue), benefitValue); + return new Voucher(id, voucherType, benefitValue); } - public VoucherTypeEnum getVoucherType() { - return voucherTypeEnum; + public VoucherType getVoucherType() { + return voucherType; } public Integer getBenefitValue() { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java new file mode 100644 index 0000000000..219c33db1e --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java @@ -0,0 +1,24 @@ +package com.programmers.springbootbasic.domain.voucher.presentation.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; + +public class UpdateVoucherRequest { + + private final VoucherType voucherType; + private final Integer benefitValue; + + public UpdateVoucherRequest(VoucherTypeEnum voucherType, Integer benefitValue) { + this.voucherType = voucherType.getVoucherType(benefitValue); + this.benefitValue = benefitValue; + } + + public VoucherType getVoucherType() { + return voucherType; + } + + public Integer getBenefitValue() { + return benefitValue; + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java index 2fc27d229a..645cee853d 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java @@ -30,4 +30,15 @@ public String toString() { """.formatted(id, voucherType, benefitValue); } + public UUID getId() { + return id; + } + + public String getVoucherType() { + return voucherType; + } + + public Integer getBenefitValue() { + return benefitValue; + } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/dto/UpdateVoucherMediatorRequest.java b/src/main/java/com/programmers/springbootbasic/mediator/dto/UpdateVoucherMediatorRequest.java new file mode 100644 index 0000000000..e55cf6c823 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/dto/UpdateVoucherMediatorRequest.java @@ -0,0 +1,42 @@ +package com.programmers.springbootbasic.mediator.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; +import java.util.UUID; + +public class UpdateVoucherMediatorRequest { + + private final UUID id; + private final VoucherTypeEnum voucherType; + private final Integer benefitValue; + + public UpdateVoucherMediatorRequest( + UUID id, VoucherTypeEnum voucherType, Integer benefitValue + ) { + this.id = id; + this.voucherType = voucherType; + this.benefitValue = benefitValue; + } + + public static UpdateVoucherMediatorRequest of( + UUID id, String voucherType, Integer benefitValue + ) { + return new UpdateVoucherMediatorRequest(id, VoucherTypeEnum.of(voucherType), benefitValue); + } + + public UpdateVoucherRequest toUpdateVoucherRequest() { + return new UpdateVoucherRequest(voucherType, benefitValue); + } + + public UUID getId() { + return id; + } + + public VoucherTypeEnum getVoucherType() { + return voucherType; + } + + public Integer getBenefitValue() { + return benefitValue; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteMyVoucher.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteMyVoucher.java new file mode 100644 index 0000000000..13c877919e --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteMyVoucher.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class DeleteMyVoucher implements MenuRequestGenerator { + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public DeleteMyVoucher(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.DELETE_VOUCHER_MINE.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(), + consoleInteractionAggregator.collectIdInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteVoucherGenerator.java new file mode 100644 index 0000000000..9b792057f9 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/DeleteVoucherGenerator.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import java.util.UUID; +import org.springframework.stereotype.Component; + +@Component +public class DeleteVoucherGenerator implements MenuRequestGenerator { + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public DeleteVoucherGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.DELETE_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(), + consoleInteractionAggregator.collectUUIDInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/UpdateVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/UpdateVoucherGenerator.java new file mode 100644 index 0000000000..783e1198b4 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/UpdateVoucherGenerator.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.mediator.dto.UpdateVoucherMediatorRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class UpdateVoucherGenerator implements MenuRequestGenerator { + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public UpdateVoucherGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.UPDATE_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest(getMenuCommand(), + consoleInteractionAggregator.collectUpdateVoucherInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java index 51cb5e7e8d..1f63e3675a 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/ViewAllVoucherGenerator.java @@ -9,7 +9,7 @@ public class ViewAllVoucherGenerator implements MenuRequestGenerator { @Override public String getMenuCommand() { - return MainMenu.LIST_VOUCHER.getCommand(); + return MainMenu.FIND_ALL_VOUCHER.getCommand(); } @Override diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java index a273f8ec6c..b072d10a82 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -41,6 +41,26 @@ public ConsoleResponse> getAllVouchers(Object... params) { SUCCESS_VOUCHER_LIST.getMessage()); } + public ConsoleResponse getVoucherById(Object... params) { + UUID id = (UUID) params[0]; + return ConsoleResponse.createWithBodyResponse( + voucherController.getVoucherById(id), + SUCCESS_VOUCHER_FOUND.getMessage()); + } + + public ConsoleResponse deleteVoucherById(Object... params) { + UUID id = (UUID) params[0]; + voucherController.deleteVoucherById(id); + return ConsoleResponse.createNoBodyResponse(SUCCESS_VOUCHER_DELETE.getMessage()); + } + + public ConsoleResponse updateVoucher(Object... params) { + UpdateVoucherMediatorRequest request = (UpdateVoucherMediatorRequest) params[0]; + + voucherController.updateVoucher(request.getId(), request.toUpdateVoucherRequest()); + return ConsoleResponse.createNoBodyResponse(SUCCESS_VOUCHER_UPDATE.getMessage()); + } + public ConsoleResponse> getBlackList(Object... params) { return ConsoleResponse.createWithBodyResponse(userController.getBlackList(), SUCCESS_BLACK_USER_LIST.getMessage()); diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index 2a60710914..8e113c3137 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -11,7 +11,10 @@ public enum MainMenu { EXIT("exit", ControllerAdapter::handleExit), CREATE_VOUCHER("create voucher", ControllerAdapter::createVoucher), - LIST_VOUCHER("list voucher", ControllerAdapter::getAllVouchers), + FIND_ALL_VOUCHER("list voucher", ControllerAdapter::getAllVouchers), + FIND_VOUCHER("find voucher", ControllerAdapter::getVoucherById), + DELETE_VOUCHER("delete voucher", ControllerAdapter::deleteVoucherById), + UPDATE_VOUCHER("update voucher", ControllerAdapter::updateVoucher), LIST_BLACK_USER("list black user", ControllerAdapter::getBlackList), ; diff --git a/src/main/java/com/programmers/springbootbasic/util/Messages.java b/src/main/java/com/programmers/springbootbasic/util/Messages.java index 0489b78332..dae02dae27 100644 --- a/src/main/java/com/programmers/springbootbasic/util/Messages.java +++ b/src/main/java/com/programmers/springbootbasic/util/Messages.java @@ -5,14 +5,31 @@ public enum Messages { SELECT_MENU(""" === Voucher Program === 종료 : exit + + === Voucher Menu === 바우처 등록 : create voucher 바우처 조회 : list voucher + 바우처 삭제 : delete voucher + 바우처 수정 : update voucher + + === User Menu === 유저 블랙리스트 조회 : list black user + 내 앞으로 바우처 등록 : register my voucher + 내 바우처 조회 : find my voucher + 내 바우처 삭제 : delete my voucher + 바우처를 소유한 유저 조회 : find users by voucher Id """), VOUCHER_REGISTER_TYPE("등록할 바우처의 타입을 입력하세요.(FIXED, PERCENT)"), VOUCHER_REGISTER_AMOUNT("등록할 바우처의 금액을 입력하세요.(숫자)"), + INPUT_ID("ID를 입력하세요."), + INPUT_NICKNAME("닉네임을 입력하세요."), + VOUCHER_UPDATE_TYPE("수정할 바우처의 타입을 입력하세요.(FIXED, PERCENT)"), + VOUCHER_UPDATE_AMOUNT("수정할 바우처의 금액을 입력하세요.(숫자)"), SUCCESS_VOUCHER_REGISTER("바우처가 등록되었습니다."), SUCCESS_VOUCHER_LIST("바우처 목록입니다."), + SUCCESS_VOUCHER_FOUND("바우처 검색 결과 입니다."), + SUCCESS_VOUCHER_DELETE("바우처가 삭제되었습니다."), + SUCCESS_VOUCHER_UPDATE("바우처가 수정되었습니다."), SUCCESS_BLACK_USER_LIST("블랙리스트 유저 목록입니다."), ; From da7d745475053fb831fad98768f0d6087570c317 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:31:24 +0900 Subject: [PATCH 49/97] =?UTF-8?q?[add]=20jdbc=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index a15d30e1ed..ad8103cbcf 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,11 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // DB + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + + // mysql + implementation 'com.mysql:mysql-connector-j' } tasks.named('test') { From 2aab19e6a5180fc8d995cf3b278e1dae4b08ed64 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:33:33 +0900 Subject: [PATCH 50/97] =?UTF-8?q?[feat]=20userVoucherWallet=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20jdbc=20=EC=A0=81=EC=9A=A9=ED=95=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/common/TimeGenerator.java | 9 ++ .../application/UserVoucherWalletService.java | 90 ++++++++++++ .../domain/UserVoucherWalletRepository.java | 15 ++ .../domain/entity/UserVoucherWallet.java | 54 +++++++ .../JdbcUserVoucherWalletRepository.java | 134 ++++++++++++++++++ .../UserVoucherWalletController.java | 35 +++++ .../dto/CreateUserVoucherWalletRequest.java | 32 +++++ .../dto/UserOwnedVoucherResponse.java | 28 ++++ .../dto/UserVoucherWalletWithUser.java | 22 +++ .../dto/UserVoucherWalletWithVoucher.java | 23 +++ .../UserVoucherWalletException.java | 10 ++ .../FindMyVoucherGenerator.java | 27 ++++ .../FindUserByVoucherGenerator.java | 28 ++++ .../FindVoucherByIdGenerator.java | 19 +++ .../RegisterUserVoucherGenerator.java | 28 ++++ .../presentation/ControllerAdapter.java | 49 ++++++- .../presentation/MainMenu.java | 4 + .../springbootbasic/util/Messages.java | 4 + .../util/ProdTimeGenerator.java | 14 ++ 19 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/programmers/springbootbasic/common/TimeGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/UserVoucherWalletRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/entity/UserVoucherWallet.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithUser.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithVoucher.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindMyVoucherGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindUserByVoucherGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindVoucherByIdGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserVoucherGenerator.java create mode 100644 src/main/java/com/programmers/springbootbasic/util/ProdTimeGenerator.java diff --git a/src/main/java/com/programmers/springbootbasic/common/TimeGenerator.java b/src/main/java/com/programmers/springbootbasic/common/TimeGenerator.java new file mode 100644 index 0000000000..85446b5e27 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/TimeGenerator.java @@ -0,0 +1,9 @@ +package com.programmers.springbootbasic.common; + +import java.time.LocalDateTime; + +public interface TimeGenerator { + + LocalDateTime now(); + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java new file mode 100644 index 0000000000..85b41a0dc1 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java @@ -0,0 +1,90 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.application; + +import static com.programmers.springbootbasic.exception.ErrorCode.FAIL_TO_DELETE_USER_VOUCHER; +import static com.programmers.springbootbasic.exception.ErrorCode.NOT_FOUND_USER; +import static com.programmers.springbootbasic.exception.ErrorCode.NOT_FOUND_USER_VOUCHER; +import static com.programmers.springbootbasic.exception.ErrorCode.NOT_FOUND_VOUCHER; + +import com.programmers.springbootbasic.common.TimeGenerator; +import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.UserVoucherWalletRepository; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.UserOwnedVoucherResponse; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithUser; +import com.programmers.springbootbasic.exception.exceptionClass.UserException; +import com.programmers.springbootbasic.exception.exceptionClass.UserVoucherWalletException; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import java.util.List; +import java.util.UUID; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class UserVoucherWalletService { + + private static final int AFFECTED_ROW_ONE = 1; + private final UserVoucherWalletRepository userVoucherWalletRepository; + private final UserRepository userRepository; + private final VoucherRepository voucherRepository; + private final TimeGenerator timeGenerator; + + public UserVoucherWalletService( + UserVoucherWalletRepository userVoucherWalletRepository, + UserRepository userRepository, + VoucherRepository voucherRepository, + TimeGenerator timeGenerator + ) { + this.userVoucherWalletRepository = userVoucherWalletRepository; + this.userRepository = userRepository; + this.voucherRepository = voucherRepository; + this.timeGenerator = timeGenerator; + } + + @Transactional + public Long create(CreateUserVoucherWalletRequest request) { + // 유효성 검사 + var user = userRepository.findByNickname(request.getNickname()) + .orElseThrow(() -> new UserException(NOT_FOUND_USER)); + var voucher = voucherRepository.findById(request.getVoucherId()) + .orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); + + var nowTime = timeGenerator.now(); + var test = request.toEntity(user.getId(), nowTime); + var a = userVoucherWalletRepository.save(request.toEntity(user.getId(), nowTime)); + return a.getId(); + } + + public List findUserByVoucherId(UUID voucherId) { + voucherRepository.findById(voucherId) + .orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); + return userVoucherWalletRepository.findUserByVoucherId(voucherId).stream() + .map(UserVoucherWalletWithUser::getUser) + .distinct() + .map(UserResponse::of) + .toList(); + } + + public List findVoucherByUserNickname(String nickname) { + var user = userRepository.findByNickname(nickname) + .orElseThrow(() -> new UserException(NOT_FOUND_USER)); + + return userVoucherWalletRepository.findVoucherByUserId(user.getId()).stream() + .map(dto -> UserOwnedVoucherResponse.of(dto.getId(), dto.getVoucher())) + .toList(); + } + + @Transactional + public void deleteById(Long id) { + userVoucherWalletRepository.findById(id) + .orElseThrow(() -> new UserVoucherWalletException(NOT_FOUND_USER_VOUCHER)); + + int affectedRow = userVoucherWalletRepository.deleteById(id); + if (affectedRow != AFFECTED_ROW_ONE) { + throw new UserVoucherWalletException(FAIL_TO_DELETE_USER_VOUCHER); + } + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/UserVoucherWalletRepository.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/UserVoucherWalletRepository.java new file mode 100644 index 0000000000..edf4e5edf6 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/UserVoucherWalletRepository.java @@ -0,0 +1,15 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.domain; + +import com.programmers.springbootbasic.common.Repository; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity.UserVoucherWallet; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithUser; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithVoucher; +import java.util.List; +import java.util.UUID; + +public interface UserVoucherWalletRepository extends Repository { + + List findVoucherByUserId(Long userId); + + List findUserByVoucherId(UUID voucherId); +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/entity/UserVoucherWallet.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/entity/UserVoucherWallet.java new file mode 100644 index 0000000000..ddaa5bbfc2 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/domain/entity/UserVoucherWallet.java @@ -0,0 +1,54 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity; + +import com.programmers.springbootbasic.common.BaseEntity; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +//TODO: 이름 변경 +public class UserVoucherWallet extends BaseEntity { + + private final Long id; + private final Long userId; + private final UUID voucherId; + + public UserVoucherWallet(Long id, Long userId, UUID voucherId, LocalDateTime createdAt) { + super(createdAt); + this.id = id; + this.userId = userId; + this.voucherId = voucherId; + } + + public UserVoucherWallet(Long userId, UUID voucherId, LocalDateTime createdAt) { + this(null, userId, voucherId, createdAt); + } + + public Long getId() { + return id; + } + + public Long getUserId() { + return userId; + } + + public UUID getVoucherId() { + return voucherId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof UserVoucherWallet that)) { + return false; + } + return Objects.equals(getId(), that.getId()) && Objects.equals(getUserId(), + that.getUserId()) && Objects.equals(getVoucherId(), that.getVoucherId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getUserId(), getVoucherId()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java new file mode 100644 index 0000000000..dee6732c6d --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java @@ -0,0 +1,134 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.infrastructure; + +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.UserVoucherWalletRepository; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity.UserVoucherWallet; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithUser; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithVoucher; +import java.sql.PreparedStatement; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +@Repository +public class JdbcUserVoucherWalletRepository implements UserVoucherWalletRepository { + + private final JdbcTemplate jdbcTemplate; + private final RowMapper voucherWalletRowMapper = + (rs, rowNum) -> new UserVoucherWallet( + rs.getLong("id"), + rs.getLong("user_id"), + UUID.fromString(rs.getString("voucher_id")), + toLocalDateTime(rs.getString("created_at")) + ); + + public JdbcUserVoucherWalletRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public UserVoucherWallet save(UserVoucherWallet entity) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection + .prepareStatement( + "INSERT INTO user_voucher_wallet (user_id, voucher_id, created_at) VALUES (?, ?, ?)", + new String[]{"id"}); + ps.setLong(1, entity.getUserId()); + ps.setString(2, entity.getVoucherId().toString()); + ps.setTimestamp(3, java.sql.Timestamp.valueOf(entity.getCreatedAt())); + return ps; + }, keyHolder); + + // key 없으면 npe + return new UserVoucherWallet( + Objects.requireNonNull(keyHolder.getKey()).longValue(), + entity.getUserId(), + entity.getVoucherId(), + entity.getCreatedAt() + ); + } + + @Override + public Optional findById(Long id) { + return jdbcTemplate.query("SELECT * FROM user_voucher_wallet WHERE id = ?", + voucherWalletRowMapper, id).stream().findFirst(); + } + + @Override + public List findAll() { + return jdbcTemplate.query("SELECT * FROM user_voucher_wallet", voucherWalletRowMapper); + } + + @Override + public int deleteById(Long id) { + return jdbcTemplate.update("DELETE FROM user_voucher_wallet WHERE id = ?", id); + } + + @Override + public int update(UserVoucherWallet entity) { + return jdbcTemplate.update( + "UPDATE user_voucher_wallet SET user_id = ? ,voucher_id = ? WHERE id = ?", + entity.getUserId(), + entity.getVoucherId().toString(), + entity.getId() + ); + } + + @Override + public List findVoucherByUserId(Long userId) { + RowMapper voucherWalletJoinRowMapper = + (rs, rowNum) -> new UserVoucherWalletWithVoucher( + rs.getLong("id"), + new Voucher( + UUID.fromString(rs.getString("voucher_id")), + VoucherTypeEnum.of(rs.getString("voucher_type")) + .getVoucherType(rs.getInt("benefit_value")), + rs.getInt("benefit_value") + ) + ); + + return jdbcTemplate.query(""" + SELECT * + FROM user_voucher_wallet uvw + JOIN vouchers v ON uvw.voucher_id = v.id + WHERE uvw.user_id = ? + """, + voucherWalletJoinRowMapper, userId); + } + + @Override + public List findUserByVoucherId(UUID voucherId) { + RowMapper voucherWalletJoinRowMapper = + (rs, rowNum) -> new UserVoucherWalletWithUser( + rs.getLong("id"), + new User( + rs.getLong("user_id"), + rs.getString("nickname"), + rs.getBoolean("blocked") + ) + ); + return jdbcTemplate.query(""" + SELECT * + FROM user_voucher_wallet uvw + JOIN users u ON uvw.user_id = u.id + WHERE uvw.voucher_id = ? + """, + voucherWalletJoinRowMapper, voucherId.toString()); + } + + private LocalDateTime toLocalDateTime(String dateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return LocalDateTime.parse(dateTime, formatter); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java new file mode 100644 index 0000000000..6f34d47d80 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java @@ -0,0 +1,35 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.presentation; + +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import com.programmers.springbootbasic.domain.userVoucherWallet.application.UserVoucherWalletService; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.UserOwnedVoucherResponse; +import java.util.List; +import java.util.UUID; +import org.springframework.stereotype.Controller; + +@Controller +public class UserVoucherWalletController { + + private final UserVoucherWalletService userVoucherWalletService; + + public UserVoucherWalletController(UserVoucherWalletService userVoucherWalletService) { + this.userVoucherWalletService = userVoucherWalletService; + } + + public void createUserVoucher(CreateUserVoucherWalletRequest request) { + userVoucherWalletService.create(request); + } + + public void deleteUserVoucher(Long id) { + userVoucherWalletService.deleteById(id); + } + + public List findUserByVoucherId(UUID voucherId) { + return userVoucherWalletService.findUserByVoucherId(voucherId); + } + + public List findVoucherByUserNickname(String nickname) { + return userVoucherWalletService.findVoucherByUserNickname(nickname); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java new file mode 100644 index 0000000000..610fd4eafc --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java @@ -0,0 +1,32 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto; + +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity.UserVoucherWallet; +import java.time.LocalDateTime; +import java.util.UUID; + +public class CreateUserVoucherWalletRequest { + + private final String nickname; + private final UUID voucherId; + + public CreateUserVoucherWalletRequest(String nickname, UUID voucherId) { + this.nickname = nickname; + this.voucherId = voucherId; + } + + public static CreateUserVoucherWalletRequest of(String nickname, UUID voucherId) { + return new CreateUserVoucherWalletRequest(nickname, voucherId); + } + + public UserVoucherWallet toEntity(Long userId, LocalDateTime createdAt) { + return new UserVoucherWallet(userId, voucherId, createdAt); + } + + public String getNickname() { + return nickname; + } + + public UUID getVoucherId() { + return voucherId; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java new file mode 100644 index 0000000000..e3346aa4c2 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; + +public class UserOwnedVoucherResponse { + + private final Long id; + private final VoucherResponse voucherResponse; + + public UserOwnedVoucherResponse(Long id, VoucherResponse voucherResponse) { + this.id = id; + this.voucherResponse = voucherResponse; + } + + public static UserOwnedVoucherResponse of(Long id, Voucher voucher) { + return new UserOwnedVoucherResponse(id, VoucherResponse.of(voucher)); + } + + @Override + public String toString() { + return """ + UserVoucher id = %s + ---Voucher Info--- + %s + """.formatted(id, voucherResponse); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithUser.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithUser.java new file mode 100644 index 0000000000..b0b583e3fe --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithUser.java @@ -0,0 +1,22 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure.dto; + +import com.programmers.springbootbasic.domain.user.domain.entity.User; + +public class UserVoucherWalletWithUser { + + private final Long id; + private final User user; + + public UserVoucherWalletWithUser(Long id, User user) { + this.id = id; + this.user = user; + } + + public Long getId() { + return id; + } + + public User getUser() { + return user; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithVoucher.java new file mode 100644 index 0000000000..37f1e9245f --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/UserVoucherWalletWithVoucher.java @@ -0,0 +1,23 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; + +// TODO: 바뀔 여지가 있음 +public class UserVoucherWalletWithVoucher { + + private final Long id; + private final Voucher voucher; + + public UserVoucherWalletWithVoucher(Long id, Voucher voucher) { + this.id = id; + this.voucher = voucher; + } + + public Long getId() { + return id; + } + + public Voucher getVoucher() { + return voucher; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java new file mode 100644 index 0000000000..2ecd7541af --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class UserVoucherWalletException extends RuntimeException { + + public UserVoucherWalletException(ErrorCode errorCode) { + super(errorCode.getMessage()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindMyVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindMyVoucherGenerator.java new file mode 100644 index 0000000000..0f8716d7db --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindMyVoucherGenerator.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class FindMyVoucherGenerator implements MenuRequestGenerator { + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public FindMyVoucherGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.FIND_VOUCHER_MINE.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(), + consoleInteractionAggregator.collectNicknameInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindUserByVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindUserByVoucherGenerator.java new file mode 100644 index 0000000000..759a74a78a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindUserByVoucherGenerator.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import java.util.UUID; +import org.springframework.stereotype.Component; + +@Component +public class FindUserByVoucherGenerator implements MenuRequestGenerator { + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public FindUserByVoucherGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.FIND_USER_BY_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(), + consoleInteractionAggregator.collectUUIDInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindVoucherByIdGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindVoucherByIdGenerator.java new file mode 100644 index 0000000000..aebdbc6020 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/FindVoucherByIdGenerator.java @@ -0,0 +1,19 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class FindVoucherByIdGenerator implements MenuRequestGenerator { + + @Override + public String getMenuCommand() { + return MainMenu.FIND_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserVoucherGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserVoucherGenerator.java new file mode 100644 index 0000000000..aa27d4ceae --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserVoucherGenerator.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class RegisterUserVoucherGenerator implements MenuRequestGenerator { + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public RegisterUserVoucherGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.CREATE_USER_VOUCHER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(), + consoleInteractionAggregator.collectUserVoucherWalletInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java index b072d10a82..ab4b2e3362 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -2,17 +2,29 @@ import static com.programmers.springbootbasic.exception.ErrorCode.EXIT; import static com.programmers.springbootbasic.util.Messages.SUCCESS_BLACK_USER_LIST; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_FOUND_BY_VOUCHER; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_VOUCHER_DELETE; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_VOUCHER_FOUND_MINE; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_VOUCHER_REGISTER; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_DELETE; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_FOUND; import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_LIST; import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_REGISTER; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_UPDATE; import com.programmers.springbootbasic.domain.user.presentation.UserController; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.UserVoucherWalletController; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.UserOwnedVoucherResponse; import com.programmers.springbootbasic.domain.voucher.presentation.VoucherController; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; import com.programmers.springbootbasic.exception.exceptionClass.SystemException; import com.programmers.springbootbasic.mediator.ConsoleResponse; +import com.programmers.springbootbasic.mediator.dto.UpdateVoucherMediatorRequest; import java.util.List; +import java.util.UUID; import org.springframework.stereotype.Component; @Component @@ -20,10 +32,15 @@ public class ControllerAdapter { private final VoucherController voucherController; private final UserController userController; + private final UserVoucherWalletController userVoucherWalletController; - public ControllerAdapter(VoucherController voucherController, UserController userController) { + public ControllerAdapter( + VoucherController voucherController, UserController userController, + UserVoucherWalletController userVoucherWalletController + ) { this.voucherController = voucherController; this.userController = userController; + this.userVoucherWalletController = userVoucherWalletController; } public ConsoleResponse handleExit(Object... params) { @@ -65,4 +82,34 @@ public ConsoleResponse> getBlackList(Object... params) { return ConsoleResponse.createWithBodyResponse(userController.getBlackList(), SUCCESS_BLACK_USER_LIST.getMessage()); } + + public ConsoleResponse createUserVoucher(Object... params) { + CreateUserVoucherWalletRequest request = (CreateUserVoucherWalletRequest) params[0]; + userVoucherWalletController.createUserVoucher(request); + return ConsoleResponse.createNoBodyResponse(SUCCESS_USER_VOUCHER_REGISTER.getMessage()); + } + + public ConsoleResponse> findUserByVoucherId(Object... params) { + UUID voucherId = (UUID) params[0]; + return ConsoleResponse.createWithBodyResponse( + userVoucherWalletController.findUserByVoucherId(voucherId), + SUCCESS_USER_FOUND_BY_VOUCHER.getMessage()); + } + + public ConsoleResponse> findVoucherByUserNickname( + Object... params + ) { + String nickname = (String) params[0]; + return ConsoleResponse.createWithBodyResponse( + userVoucherWalletController.findVoucherByUserNickname(nickname), + SUCCESS_USER_VOUCHER_FOUND_MINE.getMessage()); + } + + public ConsoleResponse deleteUserVoucherById(Object... params) { + Long id = (Long) params[0]; + userVoucherWalletController.deleteUserVoucher(id); + return ConsoleResponse.createNoBodyResponse(SUCCESS_USER_VOUCHER_DELETE.getMessage()); + } + + } diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index 8e113c3137..bb55a187c1 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -16,6 +16,10 @@ public enum MainMenu { DELETE_VOUCHER("delete voucher", ControllerAdapter::deleteVoucherById), UPDATE_VOUCHER("update voucher", ControllerAdapter::updateVoucher), LIST_BLACK_USER("list black user", ControllerAdapter::getBlackList), + CREATE_USER_VOUCHER("register my voucher", ControllerAdapter::createUserVoucher), + FIND_USER_BY_VOUCHER("find users by voucher Id", ControllerAdapter::findUserByVoucherId), + FIND_VOUCHER_MINE("find my voucher", ControllerAdapter::findVoucherByUserNickname), + DELETE_VOUCHER_MINE("delete my voucher", ControllerAdapter::deleteUserVoucherById), ; private final String command; diff --git a/src/main/java/com/programmers/springbootbasic/util/Messages.java b/src/main/java/com/programmers/springbootbasic/util/Messages.java index dae02dae27..097da7aa89 100644 --- a/src/main/java/com/programmers/springbootbasic/util/Messages.java +++ b/src/main/java/com/programmers/springbootbasic/util/Messages.java @@ -31,6 +31,10 @@ public enum Messages { SUCCESS_VOUCHER_DELETE("바우처가 삭제되었습니다."), SUCCESS_VOUCHER_UPDATE("바우처가 수정되었습니다."), SUCCESS_BLACK_USER_LIST("블랙리스트 유저 목록입니다."), + SUCCESS_USER_VOUCHER_REGISTER("요청하신 바우처가 등록되었습니다."), + SUCCESS_USER_FOUND_BY_VOUCHER("요청하신 바우처를 소유한 유저 목록입니다."), + SUCCESS_USER_VOUCHER_FOUND_MINE("소유하신 바우처 목록입니다."), + SUCCESS_USER_VOUCHER_DELETE("요청하신 바우처가 삭제되었습니다."), ; Messages(String Message) { diff --git a/src/main/java/com/programmers/springbootbasic/util/ProdTimeGenerator.java b/src/main/java/com/programmers/springbootbasic/util/ProdTimeGenerator.java new file mode 100644 index 0000000000..02f917b2f2 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/util/ProdTimeGenerator.java @@ -0,0 +1,14 @@ +package com.programmers.springbootbasic.util; + +import com.programmers.springbootbasic.common.TimeGenerator; +import java.time.LocalDateTime; +import org.springframework.stereotype.Component; + +@Component +public class ProdTimeGenerator implements TimeGenerator { + + @Override + public LocalDateTime now() { + return LocalDateTime.now(); + } +} From 069641fa7ba485b58e96dae804bd0042104e4e82 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:34:05 +0900 Subject: [PATCH 51/97] =?UTF-8?q?[change]=20jdbc=20repo=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=A1=9C=20=EC=9D=98=ED=95=9C=20profile=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/infrastructure/MemoryVoucherRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java index 98f113a9e1..d58439fe4b 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java @@ -6,8 +6,10 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; +@Profile("local") @Repository public class MemoryVoucherRepository implements VoucherRepository { From b9033dede7202d47ea78a75fb06855fba34e546c Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:34:29 +0900 Subject: [PATCH 52/97] =?UTF-8?q?[add]=20jdbc=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=98=88=EC=99=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=ED=95=B8=EB=93=A4=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/exception/AppExceptionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java index 34b41f77cd..6abee48ccd 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java @@ -1,5 +1,7 @@ package com.programmers.springbootbasic.exception; +import static com.programmers.springbootbasic.exception.ErrorCode.DATABASE_ERROR; + import com.programmers.springbootbasic.exception.exceptionClass.CustomException; import com.programmers.springbootbasic.exception.exceptionClass.FileIOException; import com.programmers.springbootbasic.exception.exceptionClass.SystemException; @@ -10,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Component; @Component @@ -56,6 +59,11 @@ public void handle() { requestProcessor.sendResponse( ConsoleResponse.createNoBodyResponse(e.getMessage()) ); + } catch (DataAccessException e) { + logger.error(String.format("DataAccess Error : %s", e.getMessage())); + requestProcessor.sendResponse( + ConsoleResponse.createNoBodyResponse(DATABASE_ERROR.getMessage()) + ); } catch (Exception e) { logger.error(String.format("Unknown Error : %s", e.getMessage())); requestProcessor.sendResponse( From eefeb07a9d937b55a7817c29c953bef461fc818f Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:34:49 +0900 Subject: [PATCH 53/97] =?UTF-8?q?[add]=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../programmers/springbootbasic/exception/ErrorCode.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java index 7a8c589d1d..05de3b7b5e 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -2,12 +2,20 @@ public enum ErrorCode { FILE_IO_ERROR("파일 에러입니다. 시스템을 종료합니다."), + DATABASE_ERROR("데이터베이스 에러입니다. 저장에 실패하였습니다. 다시 시도하세요."), INVALID_MENU("올바르지 않은 메뉴 타입입니다."), INVALID_VOUCHER("올바르지 않은 바우처 타입 입니다. 현재 등록 가능한 바우처 타입은 FIXED, PERCENT 입니다."), INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다."), INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다."), INVALID_FILE_PATH("올바르지 않은 파일 경로입니다."), EXIT("시스템을 종료합니다."), + NOT_FOUND_VOUCHER("바우처를 찾을 수 없습니다."), + NOT_FOUND_USER("사용자를 찾을 수 없습니다."), + NOT_FOUND_USER_VOUCHER("사용자 바우처를 찾을 수 없습니다."), + FAIL_TO_DELETE_VOUCHER("바우처 삭제에 실패하였습니다."), + FAIL_TO_UPDATE_VOUCHER("바우처 수정에 실패하였습니다."), + DUPLICATED_VOUCHER("이미 등록된 바우처 입니다."), + FAIL_TO_DELETE_USER_VOUCHER("사용자 바우처 삭제에 실패하였습니다."), ; ErrorCode(String message) { From ca6bedc8ed7ca871729007d31fe378ac3acebe30 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:35:17 +0900 Subject: [PATCH 54/97] =?UTF-8?q?[add]=20Long,=20UUID=20=EC=9D=B8=ED=92=8B?= =?UTF-8?q?=20=EB=B0=9B=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/IO/Console.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java index 3475206877..1509604a33 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/Console.java @@ -3,6 +3,7 @@ import com.programmers.springbootbasic.presentation.IOManager; import com.programmers.springbootbasic.presentation.validation.InputValidator; import java.util.Scanner; +import java.util.UUID; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @@ -12,11 +13,17 @@ public class Console implements IOManager { private final Scanner scanner; private final InputValidator stringValidator; private final InputValidator integerValidator; + private final InputValidator longValidator; + private final InputValidator uuidValidator; public Console( @Qualifier("getStringValidator") InputValidator stringValidator, - @Qualifier("getIntegerValidator") InputValidator integerValidator + @Qualifier("getIntegerValidator") InputValidator integerValidator, + @Qualifier("getLongValidator") InputValidator longValidator, + @Qualifier("getUUIDValidator") InputValidator uuidValidator ) { + this.longValidator = longValidator; + this.uuidValidator = uuidValidator; this.scanner = new Scanner(System.in); this.stringValidator = stringValidator; this.integerValidator = integerValidator; @@ -48,6 +55,32 @@ public int collectIntegerInput(String message) { } } + public Long collectLongInput(String message) { + while (true) { + print(message); + var result = scanner.nextLine().trim(); + var errors = longValidator.validate(result); + + if (errors.isEmpty()) { + return Long.parseLong(result); + } + errors.get().forEach(this::print); + } + } + + public UUID collectUUIDInput(String message) { + while (true) { + print(message); + var result = scanner.nextLine().trim(); + var errors = uuidValidator.validate(result); + + if (errors.isEmpty()) { + return UUID.fromString(result); + } + errors.get().forEach(this::print); + } + } + public void print(String message) { System.out.println(message); } From 6fe1726b73be1776e48d16d8a6635334b593d6b1 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:35:44 +0900 Subject: [PATCH 55/97] =?UTF-8?q?[add]=20Long,=20UUID=20input=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=EA=B8=B0=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO/ConsoleValidatorConfig.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java index e2ae42520b..2c6cc2a663 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleValidatorConfig.java @@ -1,6 +1,7 @@ package com.programmers.springbootbasic.infrastructure.IO; import com.programmers.springbootbasic.presentation.validation.InputValidator; +import java.util.UUID; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,6 +23,22 @@ public InputValidator getIntegerValidator() { return integerValidator; } + @Bean + public InputValidator getLongValidator() { + InputValidator longValidator = new InputValidator<>(); + longValidator.addValidator(this::validateString); + longValidator.addValidator(this::validateInteger); + return longValidator; + } + + @Bean + public InputValidator getUUIDValidator() { + InputValidator uuidValidator = new InputValidator<>(); + uuidValidator.addValidator(this::validateString); + uuidValidator.addValidator(this::validateUUID); + return uuidValidator; + } + private String validateString(String input) { if (input == null || input.isEmpty()) { return "입력 값이 없습니다."; @@ -38,4 +55,22 @@ private String validateInteger(String input) { } } + private String validateLong(String input) { + try { + Long.parseLong(input); + return null; + } catch (NumberFormatException e) { + return "Long 형태의 숫자가 아닙니다."; + } + } + + private String validateUUID(String input) { + try { + UUID.fromString(input); + return null; + } catch (IllegalArgumentException e) { + return "UUID 형태의 문자가 아닙니다."; + } + } + } From 69b00ca7c85d0dd09de2a25d90d06a6617f78ab5 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:36:08 +0900 Subject: [PATCH 56/97] =?UTF-8?q?[add]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=B2=98=EB=A6=AC=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO/ConsoleInteractionAggregator.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java index 5fb23d3a1a..189b5a31c0 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java @@ -1,7 +1,10 @@ package com.programmers.springbootbasic.infrastructure.IO; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.mediator.dto.UpdateVoucherMediatorRequest; import com.programmers.springbootbasic.util.Messages; +import java.util.UUID; import org.springframework.stereotype.Component; @Component @@ -20,9 +23,36 @@ public String collectMenuInput() { public CreateVoucherRequest collectVoucherInput() { String title = console.collectStringInput(Messages.VOUCHER_REGISTER_TYPE.getMessage()); int amount = console.collectIntegerInput(Messages.VOUCHER_REGISTER_AMOUNT.getMessage()); - return CreateVoucherRequest.from(title, amount); + return CreateVoucherRequest.of(title, amount); } + //TODO: 메세지 어찌할지 + public Long collectIdInput() { + return console.collectLongInput(Messages.INPUT_ID.getMessage()); + } + + public UUID collectUUIDInput() { + return console.collectUUIDInput(Messages.INPUT_ID.getMessage()); + } + + public UpdateVoucherMediatorRequest collectUpdateVoucherInput() { + UUID id = console.collectUUIDInput(Messages.INPUT_ID.getMessage()); + String voucherType = console.collectStringInput(Messages.VOUCHER_UPDATE_TYPE.getMessage()); + int amount = console.collectIntegerInput(Messages.VOUCHER_UPDATE_AMOUNT.getMessage()); + return UpdateVoucherMediatorRequest.of(id, voucherType, amount); + } + + public CreateUserVoucherWalletRequest collectUserVoucherWalletInput() { + String userNickname = console.collectStringInput(Messages.INPUT_NICKNAME.getMessage()); + UUID voucherId = console.collectUUIDInput(Messages.INPUT_ID.getMessage()); + return CreateUserVoucherWalletRequest.of(userNickname, voucherId); + } + + public String collectNicknameInput() { + return console.collectStringInput(Messages.INPUT_NICKNAME.getMessage()); + } + + public void displayMessage(String message) { console.print(message); } From 759ec6098b0cdf01c12ebe14a7f3ea6ca8d1ee4f Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:36:27 +0900 Subject: [PATCH 57/97] =?UTF-8?q?[change]=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20?= =?UTF-8?q?=EB=B3=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/programmers/springbootbasic/VoucherApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java index 98d657f55e..670aa18082 100644 --- a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java +++ b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java @@ -10,8 +10,8 @@ public class VoucherApplication { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run( - VoucherApplication.class, - args); + VoucherApplication.class, args + ); startAppWithErrorHandler(applicationContext); } From 8673a1c8bb4ccece14c386b8d076e76c9eb45170 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:37:15 +0900 Subject: [PATCH 58/97] =?UTF-8?q?[add]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +- .../domain/TestTimeGenerator.java | 12 + .../domain/TestVoucherIdGenerator.java | 12 + .../user/application/UserServiceTest.java | 47 +++ .../JdbcUserRepositoryTest.java | 266 ++++++++++++ .../user/presentation/UserControllerTest.java | 46 +++ .../UserVoucherWalletServiceTest.java | 275 +++++++++++++ .../JdbcUserVoucherWalletRepositoryTest.java | 389 ++++++++++++++++++ .../application/VoucherServiceTest.java | 167 ++++++++ .../VoucherType/FixedAmountVoucherTest.java | 38 ++ .../PercentDiscountVoucherTest.java | 43 ++ .../JdbcVoucherRepositoryTest.java | 136 ++++++ .../dto/CreateVoucherRequestTest.java | 40 ++ 13 files changed, 1479 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/programmers/springbootbasic/domain/TestTimeGenerator.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/TestVoucherIdGenerator.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepositoryTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/user/presentation/UserControllerTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucherTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucherTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java create mode 100644 src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java diff --git a/build.gradle b/build.gradle index ad8103cbcf..8a08f339ac 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,15 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter' + // Display name 쓰기 위해 + implementation 'org.junit.jupiter:junit-jupiter:5.8.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // lombok + implementation 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + // DB implementation 'org.springframework.boot:spring-boot-starter-jdbc' diff --git a/src/test/java/com/programmers/springbootbasic/domain/TestTimeGenerator.java b/src/test/java/com/programmers/springbootbasic/domain/TestTimeGenerator.java new file mode 100644 index 0000000000..f1663a6671 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/TestTimeGenerator.java @@ -0,0 +1,12 @@ +package com.programmers.springbootbasic.domain; + +import com.programmers.springbootbasic.common.TimeGenerator; +import java.time.LocalDateTime; + +public class TestTimeGenerator implements TimeGenerator { + + @Override + public LocalDateTime now() { + return LocalDateTime.of(2023, 10, 27, 15, 30, 0, 0); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/TestVoucherIdGenerator.java b/src/test/java/com/programmers/springbootbasic/domain/TestVoucherIdGenerator.java new file mode 100644 index 0000000000..32463bfa98 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/TestVoucherIdGenerator.java @@ -0,0 +1,12 @@ +package com.programmers.springbootbasic.domain; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; +import java.util.UUID; + +public class TestVoucherIdGenerator implements VoucherIdGenerator { + + @Override + public UUID generate() { + return UUID.fromString("00000000-0000-0000-0000-000000000000"); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java new file mode 100644 index 0000000000..9be4532484 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java @@ -0,0 +1,47 @@ +package com.programmers.springbootbasic.domain.user.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@DisplayName("UserService 테스트") +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserService userService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("차단 처리가 되어 있는 사용자 목록을 가져 온다.") + void success_findBlacklistedUsers() { + // given + User blockedUser = new User(1L, "user1", true); + when(userRepository.findBlacklistedUsers()).thenReturn(List.of(blockedUser)); + + // when + List result = userService.findBlacklistedUsers(); + + // then + assertThat(result).hasSize(1); + assertThat(result) + .extracting("name") + .containsExactlyInAnyOrder("user1"); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepositoryTest.java new file mode 100644 index 0000000000..7eec8d0bc4 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/user/infrastructure/JdbcUserRepositoryTest.java @@ -0,0 +1,266 @@ +package com.programmers.springbootbasic.domain.user.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@DisplayName("JdbcUserRepository 테스트") +class JdbcUserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @DisplayName("Id 값이 null인 유저가 Id 값이 할당 되어 저장 된다.") + @Test + void success_save() { + //given + User user = new User(null, "user1", false); + + //when + User result = userRepository.save(user); + + //then + assertThat(userRepository.findAll()).hasSize(1); + assertNotNull(result.getId()); + assertThat(result).extracting("nickname", "blocked") + .containsExactly("user1", false); + } + + @DisplayName("Id 값이 null이 아닌 유저를 저장해도 Id 값이 DB에 의해 생성 된다.") + @Test + void success_save_userHasId() { + //given + var previousId = -1L; + User user = new User(previousId, "user1", false); + + //when + User result = userRepository.save(user); + + //then + assertThat(userRepository.findAll()).hasSize(1); + assertNotNull(result.getId()); + assertNotEquals(result.getId(), previousId); + assertThat(result).extracting("nickname", "blocked") + .containsExactly("user1", false); + } + + @DisplayName("존재하는 User Id 값으로 User를 찾는다.") + @Test + void success_findById() { + //given + User user1 = new User(null, "user1", false); + User user2 = new User(null, "user2", false); + User user3 = new User(null, "user3", true); + userRepository.save(user1); + var savedUser2 = userRepository.save(user2); + userRepository.save(user3); + + //when + Optional result = userRepository.findById(savedUser2.getId()); + + //then + assertThat(result).isPresent(); + assertThat(result.get()).extracting("nickname", "blocked") + .containsExactly("user2", false); + } + + @DisplayName("존재하지 않는 User Id 값으로 User를 찾으면 빈 값이 반환 된다.") + @Test + void success_findById_notFound() { + //given + User user1 = new User(null, "user1", false); + User user2 = new User(null, "user2", false); + User user3 = new User(null, "user3", true); + var savedUser1 = userRepository.save(user1); + var savedUser2 = userRepository.save(user2); + var savedUser3 = userRepository.save(user3); + List existedIds = List.of(savedUser1.getId(), savedUser2.getId(), savedUser3.getId()); + + Long notExistedId = createNotExistedId(existedIds); + + //when + Optional result = userRepository.findById(notExistedId); + + //then + assertThat(result).isEmpty(); + } + + @DisplayName("모든 User를 찾는다.") + @Test + void success_findAll() { + //given + User user1 = new User(null, "user1", false); + User user2 = new User(null, "user2", false); + User user3 = new User(null, "user3", true); + userRepository.save(user1); + userRepository.save(user2); + userRepository.save(user3); + + //when + var result = userRepository.findAll(); + + //then + assertThat(result).hasSize(3); + assertThat(result).extracting("nickname", "blocked") + .containsExactlyInAnyOrder( + tuple("user1", false), + tuple("user2", false), + tuple("user3", true) + ); + } + + @DisplayName("존재하는 User Id 값으로 User를 삭제할 수 있다.") + @Test + void success_deleteById() { + //given + User user1 = new User(null, "user1", false); + var savedUser = userRepository.save(user1); + + //when + var result = userRepository.deleteById(savedUser.getId()); + + //then + assertThat(userRepository.findAll()).hasSize(0); + assertThat(result).isEqualTo(1); + } + + @DisplayName("존재하지 않는 User Id 값으로 User를 삭제하면 0을 반환 한다.") + @Test + void success_deleteById_notFound() { + //given + User user1 = new User(null, "user1", false); + var savedUser = userRepository.save(user1); + List existedIds = List.of(savedUser.getId()); + + Long notExistedId = createNotExistedId(existedIds); + + //when + var result = userRepository.deleteById(notExistedId); + + //then + assertThat(userRepository.findAll()).hasSize(1); + assertThat(result).isEqualTo(0); + } + + @DisplayName("Id 값이 존재하는 User를 수정하면 1을 반환 한다.") + @Test + void success_update() { + //given + User user1 = new User(null, "user1", false); + var savedUser = userRepository.save(user1); + + //when + var result = userRepository.update(new User(savedUser.getId(), "user2", true)); + + //then + assertThat(userRepository.findAll()).hasSize(1); + assertThat(result).isEqualTo(1); + assertThat(userRepository.findById(savedUser.getId())).isPresent() + .get().extracting("nickname", "blocked") + .containsExactly("user2", true); + } + + @DisplayName("Id 값이 존재하지 않는 User를 수정하면 0을 반환 한다.") + @Test + void success_update_notFound() { + //given + User user1 = new User(null, "user1", false); + var savedUser = userRepository.save(user1); + List existedIds = List.of(savedUser.getId()); + + Long notExistedId = createNotExistedId(existedIds); + + //when + var result = userRepository.update(new User(notExistedId, "user2", true)); + + //then + assertThat(userRepository.findAll()).hasSize(1); + assertThat(result).isEqualTo(0); + } + + @DisplayName("블랙리스트에 등록 된 유저를 찾는다.") + @Test + void success_findBlacklistedUsers() { + //given + User user1 = new User(null, "user1", false); + User user2 = new User(null, "user2", false); + User user3 = new User(null, "user3", true); + userRepository.save(user1); + userRepository.save(user2); + userRepository.save(user3); + + //when + var result = userRepository.findBlacklistedUsers(); + + //then + assertThat(result).hasSize(1); + assertThat(result).extracting("nickname", "blocked") + .containsExactly(tuple("user3", true)); + } + + @DisplayName("존재하는 닉네임으로 유저를 찾는다.") + @Test + void success_findByNickname() { + //given + User user1 = new User(null, "user1", false); + User user2 = new User(null, "user2", false); + User user3 = new User(null, "user3", true); + userRepository.save(user1); + var savedUser2 = userRepository.save(user2); + userRepository.save(user3); + + //when + var result = userRepository.findByNickname("user2"); + + //then + assertThat(result) + .isPresent() + .get() + .extracting("id", "nickname", "blocked") + .containsExactly(savedUser2.getId(), "user2", false); + } + + @DisplayName("존재하지 않는 닉네임으로 유저를 찾으면 빈 값이 반환 된다.") + @Test + void success_findByNickname_notFound() { + //given + User user1 = new User(null, "user1", false); + User user2 = new User(null, "user2", false); + User user3 = new User(null, "user3", true); + userRepository.save(user1); + userRepository.save(user2); + userRepository.save(user3); + + //when + var result = userRepository.findByNickname("user4"); + + //then + assertThat(result).isEmpty(); + } + + private static Long createNotExistedId(List existedIds) { + Random random = new Random(); + Long notExistedId = null; + do { + var randomLong = random.nextLong(); + notExistedId = randomLong & Long.MAX_VALUE; + } while (existedIds.contains(notExistedId)); + return notExistedId; + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/user/presentation/UserControllerTest.java b/src/test/java/com/programmers/springbootbasic/domain/user/presentation/UserControllerTest.java new file mode 100644 index 0000000000..8510006157 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/user/presentation/UserControllerTest.java @@ -0,0 +1,46 @@ +package com.programmers.springbootbasic.domain.user.presentation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import com.programmers.springbootbasic.domain.user.application.UserService; +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@DisplayName("UserController 테스트") +class UserControllerTest { + + @Mock + private UserService userService; + + @InjectMocks + private UserController userController; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @DisplayName("블랙리스트에 등록 된 유저가 반환 된다.") + @Test + void success_getBlackList() { + // given + List mockBlacklist = List.of( + new UserResponse("User1"), + new UserResponse("User2") + ); + when(userService.findBlacklistedUsers()).thenReturn(mockBlacklist); + + // when + List resultBlacklist = userController.getBlackList(); + + // then + assertEquals(mockBlacklist, resultBlacklist); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java new file mode 100644 index 0000000000..2a249b1e51 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java @@ -0,0 +1,275 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.programmers.springbootbasic.common.TimeGenerator; +import com.programmers.springbootbasic.domain.TestTimeGenerator; +import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.UserVoucherWalletRepository; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity.UserVoucherWallet; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.FixedAmountVoucher; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithUser; +import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithVoucher; +import com.programmers.springbootbasic.exception.exceptionClass.UserException; +import com.programmers.springbootbasic.exception.exceptionClass.UserVoucherWalletException; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +@ActiveProfiles("test") +@DisplayName("UserVoucherWalletService 테스트") +class UserVoucherWalletServiceTest { + + @InjectMocks + private UserVoucherWalletService userVoucherWalletService; + + @Mock + private UserVoucherWalletRepository userVoucherWalletRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private VoucherRepository voucherRepository; + + private TimeGenerator timeGenerator; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + this.timeGenerator = new TestTimeGenerator(); + ReflectionTestUtils.setField(userVoucherWalletService, "timeGenerator", timeGenerator); + } + + @DisplayName("존재 하는 유저가 존재 하는 쿠폰을 등록 할 수 있다.") + @Test + void success_create() { + // given + var existedUUID = UUID.randomUUID(); + var existedNickname = "nickname"; + var userId = 1L; + var userVoucherWalletId = 1L; + CreateUserVoucherWalletRequest request = new CreateUserVoucherWalletRequest( + existedNickname, existedUUID + ); + when(userRepository.findByNickname(existedNickname)).thenReturn( + Optional.of(new User(userId, existedNickname, false))); + when(voucherRepository.findById(existedUUID)).thenReturn( + Optional.of( + new Voucher(existedUUID, new FixedAmountVoucher(100), 100) + ) + ); + when(userVoucherWalletRepository.save(any())) + .thenReturn( + new UserVoucherWallet(userVoucherWalletId, userId, existedUUID, timeGenerator.now()) + ); + + // when + var result = userVoucherWalletService.create(request); + + // then + assertThat(result).isEqualTo(userVoucherWalletId); + } + + @DisplayName("존재하지 않는 유저가 쿠폰을 등록 하려하면 예외가 발생한다.") + @Test + void fail_create_notExistedUser() { + // given + var existedUUID = UUID.randomUUID(); + var notExistedNickname = "notExistedNickname"; + CreateUserVoucherWalletRequest request = new CreateUserVoucherWalletRequest( + notExistedNickname, existedUUID + ); + when(userRepository.findByNickname(notExistedNickname)).thenReturn(Optional.empty()); + + // when + // then + assertThatThrownBy(() -> userVoucherWalletService.create(request)) + .isInstanceOf(UserException.class) + .hasMessageContaining("사용자를 찾을 수 없습니다."); + } + + @DisplayName("존재하지 않는 쿠폰을 등록 하려 하면 예외가 발생한다.") + @Test + void success_create_notExistedVoucher() { + // given + var notExistedUUID = UUID.randomUUID(); + var existedNickname = "existedNickname"; + CreateUserVoucherWalletRequest request = new CreateUserVoucherWalletRequest( + existedNickname, notExistedUUID + ); + when(userRepository.findByNickname(existedNickname)).thenReturn( + Optional.of(new User(1L, existedNickname, false))); + when(voucherRepository.findById(notExistedUUID)).thenReturn(Optional.empty()); + + // when + // then + assertThatThrownBy(() -> userVoucherWalletService.create(request)) + .isInstanceOf(VoucherException.class) + .hasMessageContaining("바우처를 찾을 수 없습니다."); + } + + @DisplayName("존재하는 바우처 아이디로 바우처를 소유한 유저를 조회 할 수 있다.") + @Test + void success_findUserByVoucherId() { + // given + var existedUUID = UUID.randomUUID(); + when(voucherRepository.findById(existedUUID)).thenReturn( + Optional.of(new Voucher(existedUUID, new FixedAmountVoucher(100), 100)) + ); + when(userVoucherWalletRepository.findUserByVoucherId(existedUUID)).thenReturn( + List.of( + new UserVoucherWalletWithUser(1L, new User(1L, "nickname1", false)), + new UserVoucherWalletWithUser(2L, new User(2L, "nickname2", false)) + ) + ); + + // when + var result = userVoucherWalletService.findUserByVoucherId(existedUUID); + + // then + assertThat(result).hasSize(2); + } + + @DisplayName("바우처를 소유한 유저를 조회 할 때, 중복된 유저는 제거된다.") + @Test + void success_findUserByVoucherId_duplicatedUserVoucher() { + // given + var existedUUID = UUID.randomUUID(); + when(voucherRepository.findById(existedUUID)).thenReturn( + Optional.of(new Voucher(existedUUID, new FixedAmountVoucher(100), 100)) + ); + when(userVoucherWalletRepository.findUserByVoucherId(existedUUID)).thenReturn( + List.of( + new UserVoucherWalletWithUser(1L, new User(1L, "nickname1", false)), + new UserVoucherWalletWithUser(2L, new User(1L, "nickname1", false)) + ) + ); + + // when + var result = userVoucherWalletService.findUserByVoucherId(existedUUID); + + // then + assertThat(result).hasSize(1); + } + + @DisplayName("바우처를 소유한 유저를 조회 할때 존재하지 않는 바우처 아이디 조회하면 예외가 발생한다.") + @Test + void fail_findUserByVoucherId_notExistedVoucher() { + // given + var notExistedUUID = UUID.randomUUID(); + when(voucherRepository.findById(notExistedUUID)).thenReturn(Optional.empty()); + + // when + // then + assertThatThrownBy(() -> userVoucherWalletService.findUserByVoucherId(notExistedUUID)) + .isInstanceOf(VoucherException.class) + .hasMessageContaining("바우처를 찾을 수 없습니다."); + } + + @DisplayName("존재하는 유저의 닉네임으로 소유한 바우처를 조회 할 수 있다.") + @Test + void success_findVoucherByUserNickname() { + // given + var existedNickname = "nickname"; + var userId = 1L; + when(userRepository.findByNickname(existedNickname)).thenReturn( + Optional.of(new User(userId, existedNickname, false)) + ); + when(userVoucherWalletRepository.findVoucherByUserId(userId)).thenReturn( + List.of( + new UserVoucherWalletWithVoucher(1L, new Voucher(UUID.randomUUID(), + new FixedAmountVoucher(100), 100)), + new UserVoucherWalletWithVoucher(2L, new Voucher(UUID.randomUUID(), + new FixedAmountVoucher(100), 100)) + ) + ); + + // when + var result = userVoucherWalletService.findVoucherByUserNickname(existedNickname); + + // then + assertThat(result).hasSize(2); + } + + @DisplayName("존재하지 않는 유저의 닉네임으로 소유한 바우처를 조회 하려하면 예외가 발생한다.") + @Test + void fail_findVoucherByUserNickname_notExistedUser() { + // given + var notExistedNickname = "notExistedNickname"; + when(userRepository.findByNickname(notExistedNickname)).thenReturn(Optional.empty()); + + // when + // then + assertThatThrownBy( + () -> userVoucherWalletService.findVoucherByUserNickname(notExistedNickname)) + .isInstanceOf(UserException.class) + .hasMessageContaining("사용자를 찾을 수 없습니다."); + } + + @DisplayName("id로 소유한 바우처를 삭제 할 수 있다.") + @Test + void success_deleteById() { + // given + var existedId = 1L; + when(userVoucherWalletRepository.findById(existedId)).thenReturn( + Optional.of( + new UserVoucherWallet(existedId, 1L, UUID.randomUUID(), timeGenerator.now())) + ); + when(userVoucherWalletRepository.deleteById(existedId)).thenReturn(1); + + // when + // then + assertThatCode(() -> userVoucherWalletService.deleteById(existedId)) + .doesNotThrowAnyException(); + } + + @DisplayName("존재하지 않는 id로 소유한 바우처를 삭제 하려하면 예외가 발생한다.") + @Test + void fail_deleteById_notExistedId() { + // given + var notExistedId = 1L; + when(userVoucherWalletRepository.findById(notExistedId)).thenReturn(Optional.empty()); + + // when + // then + assertThatThrownBy(() -> userVoucherWalletService.deleteById(notExistedId)) + .isInstanceOf(UserVoucherWalletException.class) + .hasMessageContaining("사용자 바우처를 찾을 수 없습니다."); + } + + @DisplayName("존재하는 id로 소유한 바우처를 삭제 할 때, 삭제된 바우처가 없으면 예외가 발생한다.") + @Test + void success_deleteById_notDeletedUserVoucher() { + // given + var existedId = 1L; + when(userVoucherWalletRepository.findById(existedId)).thenReturn( + Optional.of( + new UserVoucherWallet(existedId, 1L, UUID.randomUUID(), timeGenerator.now())) + ); + when(userVoucherWalletRepository.deleteById(existedId)).thenReturn(0); + + // when + // then + assertThatThrownBy(() -> userVoucherWalletService.deleteById(existedId)) + .isInstanceOf(UserVoucherWalletException.class) + .hasMessageContaining("바우처 삭제에 실패하였습니다."); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java new file mode 100644 index 0000000000..b0cbecb5df --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java @@ -0,0 +1,389 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.programmers.springbootbasic.common.TimeGenerator; +import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.domain.entity.User; +import com.programmers.springbootbasic.domain.TestTimeGenerator; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.UserVoucherWalletRepository; +import com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity.UserVoucherWallet; +import com.programmers.springbootbasic.domain.voucher.domain.ProdVoucherIdGenerator; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.FixedAmountVoucher; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.List; +import java.util.Random; +import java.util.UUID; +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.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@DisplayName("JdbcUserVoucherWalletRepository 테스트") +class JdbcUserVoucherWalletRepositoryTest { + + @Autowired + private UserVoucherWalletRepository jdbcUserVoucherWalletRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private VoucherRepository voucherRepository; + private final TimeGenerator timeGenerator = new TestTimeGenerator(); + private final VoucherIdGenerator idGenerator = new ProdVoucherIdGenerator(); + + @DisplayName("Id 값이 null인 UserVoucherWallet가 Id 값이 할당 되어 저장 된다.") + @Test + void success_save() { + // given + var userVoucherWallet = createUserVoucherWallet(); + + // when + var result = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + + // then + assertThat(jdbcUserVoucherWalletRepository.findAll()).hasSize(1); + assertNotNull(result.getId()); + assertThat(result).extracting("userId", "voucherId", "createdAt") + .containsExactly(userVoucherWallet.getUserId(), userVoucherWallet.getVoucherId(), + userVoucherWallet.getCreatedAt()); + } + + @DisplayName("Id 값이 null이 아닌 UserVoucherWallet를 저장해도 Id 값이 DB에 의해 생성 된다.") + @Test + void success_save_userVoucherWalletHasId() { + // given + var previousId = -1L; + var user = saveUser("user1"); + var voucher = saveVoucher(idGenerator.generate()); + var userVoucherWallet = new UserVoucherWallet(previousId, user.getId(), voucher.getId(), + timeGenerator.now()); + + // when + var result = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + + // then + assertThat(jdbcUserVoucherWalletRepository.findAll()).hasSize(1); + assertNotNull(result.getId()); + assertNotEquals(result.getId(), previousId); + assertThat(result).extracting("userId", "voucherId", "createdAt") + .containsExactly(userVoucherWallet.getUserId(), userVoucherWallet.getVoucherId(), + userVoucherWallet.getCreatedAt()); + } + + @DisplayName("존재하지 않는 User Id 값으로 UserVoucherWallet를 저장하면 예외가 발생한다.") + @Test + void fail_save_userIdNotExist() { + // given + var notExistUserId = 1L; + var voucher = saveVoucher(idGenerator.generate()); + var userVoucherWallet = new UserVoucherWallet(null, notExistUserId, voucher.getId(), + timeGenerator.now()); + + // when & then + assertThatThrownBy(() -> jdbcUserVoucherWalletRepository.save(userVoucherWallet)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @DisplayName("존재하지 않는 Voucher Id 값으로 UserVoucherWallet를 저장하면 예외가 발생한다.") + @Test + void fail_save_voucherIdNotExist() { + // given + var user = saveUser("user1"); + var notExistVoucherId = idGenerator.generate(); + var userVoucherWallet = new UserVoucherWallet(null, user.getId(), notExistVoucherId, + timeGenerator.now()); + + // when & then + assertThatThrownBy(() -> jdbcUserVoucherWalletRepository.save(userVoucherWallet)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @DisplayName("존재하는 UserVoucherWallet Id 값으로 UserVoucherWallet를 찾을 수 있다.") + @Test + void success_findById() { + // given + var userVoucherWallet = createUserVoucherWallet(); + var savedUserVoucherWallet = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + + // when + var result = jdbcUserVoucherWalletRepository.findById(savedUserVoucherWallet.getId()); + + // then + assertThat(result).isPresent(); + assertThat(result.get()).extracting("userId", "voucherId", "createdAt") + .containsExactly(userVoucherWallet.getUserId(), userVoucherWallet.getVoucherId(), + userVoucherWallet.getCreatedAt()); + } + + @DisplayName("모든 UserVoucherWallet를 찾을 수 있다.") + @Test + void success_findAll() { + // given + var userVoucherWallet1 = createUserVoucherWallet("user1", idGenerator.generate()); + var userVoucherWallet2 = createUserVoucherWallet("user2", idGenerator.generate()); + var userVoucherWallet3 = createUserVoucherWallet("user3", idGenerator.generate()); + jdbcUserVoucherWalletRepository.save(userVoucherWallet1); + jdbcUserVoucherWalletRepository.save(userVoucherWallet2); + jdbcUserVoucherWalletRepository.save(userVoucherWallet3); + + // when + var result = jdbcUserVoucherWalletRepository.findAll(); + + // then + assertThat(result).hasSize(3); + assertThat(result).extracting("userId", "voucherId", "createdAt") + .containsExactlyInAnyOrder( + tuple(userVoucherWallet1.getUserId(), userVoucherWallet1.getVoucherId(), + userVoucherWallet1.getCreatedAt()), + tuple(userVoucherWallet2.getUserId(), userVoucherWallet2.getVoucherId(), + userVoucherWallet2.getCreatedAt()), + tuple(userVoucherWallet3.getUserId(), userVoucherWallet3.getVoucherId(), + userVoucherWallet3.getCreatedAt()) + ); + } + + @DisplayName("존재하는 UserVoucherWallet Id 값으로 UserVoucherWallet를 삭제할 수 있다.") + @Test + void success_deleteById() { + // given + var userVoucherWallet = createUserVoucherWallet(); + var savedUserVoucherWallet = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + + // when + var result = jdbcUserVoucherWalletRepository.deleteById(savedUserVoucherWallet.getId()); + + // then + assertThat(result).isEqualTo(1); + assertThat(jdbcUserVoucherWalletRepository.findAll()).isEmpty(); + } + + @DisplayName("존재하지 않는 UserVoucherWallet Id 값으로 UserVoucherWallet를 삭제하면 0을 반환 한다.") + @Test + void fail_deleteById_notFound() { + // given + var userVoucherWallet = createUserVoucherWallet(); + var savedUserVoucherWallet = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + Long notExistId = createNotExistedId(List.of(savedUserVoucherWallet.getId())); + + // when + var result = jdbcUserVoucherWalletRepository.deleteById(notExistId); + + // then + assertThat(result).isEqualTo(0); + assertThat(jdbcUserVoucherWalletRepository.findAll()).hasSize(1); + } + + @DisplayName("존재하는 User와 Voucher id 값으로 UserVoucherWallet를 수정할 수 있다.") + @Test + void success_update() { + // given + var userVoucherWallet = createUserVoucherWallet(); + var savedUserVoucherWallet = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + + var newVoucherId = saveVoucher(idGenerator.generate()).getId(); + var newUserId = saveUser("user2").getId(); + var newUserVoucherWallet = new UserVoucherWallet(savedUserVoucherWallet.getId(), + newUserId, newVoucherId, savedUserVoucherWallet.getCreatedAt()); + + // when + var result = jdbcUserVoucherWalletRepository.update(newUserVoucherWallet); + + // then + assertThat(result).isEqualTo(1); + assertThat(jdbcUserVoucherWalletRepository.findAll()).hasSize(1); + assertThat(jdbcUserVoucherWalletRepository.findById(savedUserVoucherWallet.getId())) + .isPresent() + .get() + .extracting("userId", "voucherId", "createdAt") + .containsExactly(newUserVoucherWallet.getUserId(), newUserVoucherWallet.getVoucherId(), + newUserVoucherWallet.getCreatedAt()); + } + + @DisplayName("존재하지 않는 User와 Voucher id 값으로 UserVoucherWallet를 수정하면 예외를 발생한다.") + @Test + void fail_update_notFound() { + // given + var userVoucherWallet = createUserVoucherWallet(); + var savedUserVoucherWallet = jdbcUserVoucherWalletRepository.save(userVoucherWallet); + + var newVoucherId = idGenerator.generate(); + var newUserId = createNotExistedId(List.of(savedUserVoucherWallet.getUserId())); + var newUserVoucherWallet = new UserVoucherWallet(savedUserVoucherWallet.getId(), newUserId, + newVoucherId, + savedUserVoucherWallet.getCreatedAt()); + + // when & then + assertThatThrownBy(() -> jdbcUserVoucherWalletRepository.update(newUserVoucherWallet)) + .isInstanceOf(DataAccessException.class); + } + + @DisplayName("존재하는 User Id 값으로 해당 유저가 가진 Voucher들을 찾을 수 있다.") + @Test + void success_findVoucherByUserId() { + // given + var user1 = saveUser("user1"); + var user2 = saveUser("user2"); + var voucher1 = saveVoucher(idGenerator.generate()); + var voucher2 = saveVoucher(idGenerator.generate()); + var voucher3 = saveVoucher(idGenerator.generate()); + // user1 + var userVoucherWallet1 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var userVoucherWallet2 = new UserVoucherWallet(null, user1.getId(), voucher2.getId(), + timeGenerator.now()); + // user2 + var userVoucherWallet3 = new UserVoucherWallet(null, user2.getId(), voucher3.getId(), + timeGenerator.now()); + var savedUserVoucherWallet1 = jdbcUserVoucherWalletRepository.save(userVoucherWallet1); + var savedUserVoucherWallet2 = jdbcUserVoucherWalletRepository.save(userVoucherWallet2); + var savedUserVoucherWallet3 = jdbcUserVoucherWalletRepository.save(userVoucherWallet3); + + // when + var result = jdbcUserVoucherWalletRepository.findVoucherByUserId(user1.getId()); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting("id", "voucher") + .containsExactlyInAnyOrder( + tuple(savedUserVoucherWallet1.getId(), voucher1), + tuple(savedUserVoucherWallet2.getId(), voucher2) + ); + } + + @DisplayName("유저가 소유한 Voucher들을 조회 할때 같은 바우처를 여러개 가질 수 있다.") + @Test + void success_findVoucherByUserId_sameVoucher() { + // given + var user1 = saveUser("user1"); + var voucher1 = saveVoucher(idGenerator.generate()); + + var userVoucherWallet1 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var userVoucherWallet2 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var userVoucherWallet3 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var savedUserVoucherWallet1 = jdbcUserVoucherWalletRepository.save(userVoucherWallet1); + var savedUserVoucherWallet2 = jdbcUserVoucherWalletRepository.save(userVoucherWallet2); + var savedUserVoucherWallet3 = jdbcUserVoucherWalletRepository.save(userVoucherWallet3); + + // when + var result = jdbcUserVoucherWalletRepository.findVoucherByUserId(user1.getId()); + + // then + assertThat(result).hasSize(3); + assertThat(result).extracting("id", "voucher") + .containsExactlyInAnyOrder( + tuple(savedUserVoucherWallet1.getId(), voucher1), + tuple(savedUserVoucherWallet2.getId(), voucher1), + tuple(savedUserVoucherWallet3.getId(), voucher1) + ); + } + + @DisplayName("존재하는 Voucher Id 값으로 해당 Voucher를 가진 유저들을 찾을 수 있다.") + @Test + void success_findUserByVoucherId() { + // given + var user1 = saveUser("user1"); + var user2 = saveUser("user2"); + var voucher1 = saveVoucher(idGenerator.generate()); + var voucher2 = saveVoucher(idGenerator.generate()); + // user1 + var userVoucherWallet1 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var userVoucherWallet2 = new UserVoucherWallet(null, user1.getId(), voucher2.getId(), + timeGenerator.now()); + // user2 + var userVoucherWallet3 = new UserVoucherWallet(null, user2.getId(), voucher1.getId(), + timeGenerator.now()); + var savedUserVoucherWallet1 = jdbcUserVoucherWalletRepository.save(userVoucherWallet1); + jdbcUserVoucherWalletRepository.save(userVoucherWallet2); + var savedUserVoucherWallet3 = jdbcUserVoucherWalletRepository.save(userVoucherWallet3); + + // when + var result = jdbcUserVoucherWalletRepository.findUserByVoucherId(voucher1.getId()); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting("id", "user") + .containsExactlyInAnyOrder( + tuple(savedUserVoucherWallet1.getId(), user1), + tuple(savedUserVoucherWallet3.getId(), user2) + ); + } + + @DisplayName("특정 Voucher를 가진 유저들을 조회 할때 같은 유저가 여러개 있을 수 있다.") + @Test + void success_findUserByVoucherId_sameUser() { + // given + var user1 = saveUser("user1"); + var voucher1 = saveVoucher(idGenerator.generate()); + + var userVoucherWallet1 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var userVoucherWallet2 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var userVoucherWallet3 = new UserVoucherWallet(null, user1.getId(), voucher1.getId(), + timeGenerator.now()); + var savedUserVoucherWallet1 = jdbcUserVoucherWalletRepository.save(userVoucherWallet1); + var savedUserVoucherWallet2 = jdbcUserVoucherWalletRepository.save(userVoucherWallet2); + var savedUserVoucherWallet3 = jdbcUserVoucherWalletRepository.save(userVoucherWallet3); + + // when + var result = jdbcUserVoucherWalletRepository.findUserByVoucherId(voucher1.getId()); + + // then + assertThat(result).hasSize(3); + assertThat(result).extracting("id", "user") + .containsExactlyInAnyOrder( + tuple(savedUserVoucherWallet1.getId(), user1), + tuple(savedUserVoucherWallet2.getId(), user1), + tuple(savedUserVoucherWallet3.getId(), user1) + ); + } + + private User saveUser(String nickname) { + return userRepository.save(new User(null, nickname, false)); + } + + private Voucher saveVoucher(UUID id) { + return voucherRepository.save( + new Voucher(id, new FixedAmountVoucher(100), 100) + ); + } + + private UserVoucherWallet createUserVoucherWallet(String nickname, UUID id) { + var user = saveUser(nickname); + var voucher = saveVoucher(id); + var userVoucherWallet = new UserVoucherWallet(null, user.getId(), voucher.getId(), + timeGenerator.now()); + return userVoucherWallet; + } + + private UserVoucherWallet createUserVoucherWallet() { + return createUserVoucherWallet("user1", idGenerator.generate()); + } + + private static Long createNotExistedId(List existedIds) { + Random random = new Random(); + Long notExistedId = null; + do { + var randomLong = random.nextLong(); + notExistedId = randomLong & Long.MAX_VALUE; + } while (existedIds.contains(notExistedId)); + return notExistedId; + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java new file mode 100644 index 0000000000..d1425a139b --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java @@ -0,0 +1,167 @@ +package com.programmers.springbootbasic.domain.voucher.application; + +import static com.programmers.springbootbasic.exception.ErrorCode.NOT_FOUND_VOUCHER; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.programmers.springbootbasic.domain.TestVoucherIdGenerator; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.util.ReflectionTestUtils; + +class VoucherServiceTest { + + @InjectMocks + private VoucherService voucherService; + @Mock + private VoucherRepository voucherRepository; + private VoucherIdGenerator idGenerator; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + this.idGenerator = new TestVoucherIdGenerator(); + ReflectionTestUtils.setField(voucherService, "idGenerator", idGenerator); + } + + @DisplayName("유효한 정보의 바우처를 생성 할 수 있다.") + @Test + void success_create() { + // given + CreateVoucherRequest request = CreateVoucherRequest.of(VoucherTypeEnum.FIXED.name(), 10); + when(voucherRepository.save(any(Voucher.class))).thenReturn( + new Voucher(idGenerator.generate(), request.getVoucherType(), + request.getBenefitValue())); + + // when + var result = voucherService.create(request); + + // then + assertEquals(result, idGenerator.generate()); + } + + @DisplayName("모든 바우처를 조회 할 수 있다.") + @Test + void success_findAll() { + // given + when(voucherRepository.findAll()).thenReturn( + List.of( + new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), 10), + new Voucher(idGenerator.generate(), VoucherTypeEnum.PERCENT.getVoucherType(10), 10) + ) + ); + + // when + var result = voucherService.findAll(); + + // then + assertEquals(2, result.size()); + for (Object item : result) { + assertTrue(item instanceof VoucherResponse); + } + } + + @DisplayName("존재하는 바우처를 id로 조회 할 수 있다.") + @Test + void success_findById() { + // given + var voucher = new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), + 10); + when(voucherRepository.findById(idGenerator.generate())).thenReturn( + java.util.Optional.of(voucher)); + + // when + var result = voucherService.findById(voucher.getId()); + + // then + assertEquals(result.getId(), voucher.getId()); + } + + @DisplayName("존재하지 않는 바우처를 id로 조회 하려 하면 예외를 발생한다.") + @Test + void fail_findById() { + // given + when(voucherRepository.findById(any())).thenReturn(java.util.Optional.empty()); + + // when && then + assertThatThrownBy(() -> voucherService.findById(idGenerator.generate())) + .isInstanceOf(VoucherException.class) + .hasMessage(NOT_FOUND_VOUCHER.getMessage()); + } + + @DisplayName("존재하는 바우처를 id로 삭제 할 수 있다.") + @Test + void success_deleteById() { + // given + var voucher = new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), + 10); + when(voucherRepository.findById(voucher.getId())).thenReturn(Optional.of(voucher)); + when(voucherRepository.deleteById(voucher.getId())).thenReturn(1); + + // when && then + assertThatCode(() -> voucherService.deleteById(voucher.getId())) + .doesNotThrowAnyException(); + } + + @DisplayName("존재하지 않는 바우처를 id로 삭제 하려 하면 예외를 발생한다.") + @Test + void fail_deleteById() { + // given + when(voucherRepository.findById(any())).thenReturn(java.util.Optional.empty()); + + // when && then + assertThatThrownBy(() -> voucherService.deleteById(idGenerator.generate())) + .isInstanceOf(VoucherException.class) + .hasMessageContaining(NOT_FOUND_VOUCHER.getMessage()); + } + + @DisplayName("존재하는 바우처를 유효한 데이터로 수정 할 수 있다.") + @Test + void success_update() { + // given + var voucher = new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), + 10); + UpdateVoucherRequest request = new UpdateVoucherRequest(VoucherTypeEnum.PERCENT, 10); + when(voucherRepository.findById(voucher.getId())).thenReturn( + java.util.Optional.of(voucher)); + when(voucherRepository.update(any(Voucher.class))).thenReturn(1); + + // when + var result = voucherService.update(voucher.getId(), request); + + // then + assertEquals(result, voucher.getId()); + } + + @DisplayName("존재하지 않는 바우처를 유효한 데이터로 수정 하려 하면 예외를 발생한다.") + @Test + void fail_update_voucherNotFound() { + // given + UpdateVoucherRequest request = new UpdateVoucherRequest(VoucherTypeEnum.PERCENT, 10); + when(voucherRepository.findById(any())).thenReturn(java.util.Optional.empty()); + + // when && then + assertThatThrownBy(() -> voucherService.update(idGenerator.generate(), request)) + .isInstanceOf(VoucherException.class) + .hasMessageContaining(NOT_FOUND_VOUCHER.getMessage()); + } + +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucherTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucherTest.java new file mode 100644 index 0000000000..01fbf6278e --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/FixedAmountVoucherTest.java @@ -0,0 +1,38 @@ +package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_FIXED_VOUCHER_BENEFIT; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class FixedAmountVoucherTest { + + @DisplayName("유효한 데이터로 FixedAmountVoucher 인스턴스를 생성 할 수 있다.") + @Test + void success_createInstance() { + // given + Integer benefit = 10; + + // when + var result = new FixedAmountVoucher(benefit); + + // then + assertEquals(result.getVoucherTypeName(), "FIXED"); + } + + @DisplayName("benefit이 0 이하인 FixedAmountVoucher 인스턴스를 생성 할 수 없다.") + @Test + void fail_createInstance() { + // given + Integer benefit = -1; + + // when && then + assertThatThrownBy(() -> new FixedAmountVoucher(benefit)) + .isInstanceOf(VoucherException.class) + .hasMessageContaining(INVALID_FIXED_VOUCHER_BENEFIT.getMessage()); + } + +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucherTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucherTest.java new file mode 100644 index 0000000000..19b9e5d268 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/PercentDiscountVoucherTest.java @@ -0,0 +1,43 @@ +package com.programmers.springbootbasic.domain.voucher.domain.VoucherType; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_PERCENT_VOUCHER_BENEFIT; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PercentDiscountVoucherTest { + + @DisplayName("유효한 데이터로 PercentDiscountVoucher 인스턴스를 생성 할 수 있다.") + @Test + void success_createInstance() { + // given + Integer benefit = 10; + + // when + var result = new PercentDiscountVoucher(benefit); + + // then + assertEquals(result.getVoucherTypeName(), "PERCENT"); + } + + @DisplayName("benefit이 0 이하 100 초과인 PercentDiscountVoucher 인스턴스를 생성 할 수 없다.") + @Test + void fail_createInstance() { + // given + Integer benefit1 = -1; + Integer benefit2 = 101; + + // when && then + assertThatThrownBy(() -> new PercentDiscountVoucher(benefit1)) + .isInstanceOf(VoucherException.class) + .hasMessageContaining(INVALID_PERCENT_VOUCHER_BENEFIT.getMessage()); + + assertThatThrownBy(() -> new PercentDiscountVoucher(benefit2)) + .isInstanceOf(VoucherException.class) + .hasMessageContaining(INVALID_PERCENT_VOUCHER_BENEFIT.getMessage()); + } + +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java new file mode 100644 index 0000000000..d42a63d030 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java @@ -0,0 +1,136 @@ +package com.programmers.springbootbasic.domain.voucher.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.programmers.springbootbasic.domain.voucher.domain.ProdVoucherIdGenerator; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.FixedAmountVoucher; +import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.util.UUID; +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.dao.DataAccessException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +@DisplayName("JdbcVoucherRepositoryTest 테스트") +class JdbcVoucherRepositoryTest { + + @Autowired + private VoucherRepository voucherRepository; + private final VoucherIdGenerator idGenerator = new ProdVoucherIdGenerator(); + + @DisplayName("Voucher를 저장할 수 있다.") + @Test + void success_save() { + // given + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + + // when + Voucher savedVoucher = voucherRepository.save(voucher); + + // then + assertThat(voucherRepository.findAll()).hasSize(1); + assertThat(savedVoucher).isEqualTo(voucher); + } + + @DisplayName("존재하는 Voucher Id 값으로 Voucher를 조회할 수 있다.") + @Test + void success_findById() { + // given + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + voucherRepository.save(voucher); + + // when + var result = voucherRepository.findById(voucher.getId()); + + // then + assertThat(result).isPresent().get().isEqualTo(voucher); + } + + @DisplayName("모든 Voucher를 조회할 수 있다.") + @Test + void success_findAll() { + // given + Voucher voucher1 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + Voucher voucher2 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(200), 200); + voucherRepository.save(voucher1); + voucherRepository.save(voucher2); + + // when + var result = voucherRepository.findAll(); + + // then + assertThat(result).hasSize(2); + } + + @DisplayName("존재하는 Voucher Id 값으로 Voucher를 삭제할 수 있다.") + @Test + void success_deleteById() { + // given + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + voucherRepository.save(voucher); + + // when + int result = voucherRepository.deleteById(voucher.getId()); + + // then + assertThat(result).isEqualTo(1); + assertThat(voucherRepository.findAll()).isEmpty(); + } + + @DisplayName("존재하지 않는 ID로 Voucher를 삭제할 때, 0을 반환한다.") + @Test + void fail_deleteById_notFound() { + // given + UUID nonExistentId = idGenerator.generate(); + + // when + int result = voucherRepository.deleteById(nonExistentId); + + // then + assertThat(result).isZero(); + } + + @DisplayName("Voucher 정보를 업데이트 할 수 있다.") + @Test + void success_update() { + // given + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + voucherRepository.save(voucher); + Voucher updatedVoucher = new Voucher(voucher.getId(), new FixedAmountVoucher(200), 200); + + // when + int result = voucherRepository.update(updatedVoucher); + + // then + assertThat(result).isEqualTo(1); + assertThat(voucherRepository.findById(voucher.getId()).get().getBenefitValue()) + .isEqualTo(200); + } + + @DisplayName("누락된 필드 정보로 Voucher를 업데이트 할 때, 예외가 발생한다.") + @Test + void fail_update_missingFields() { + // given + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + voucherRepository.save(voucher); + Voucher incompleteVoucher = new Voucher(voucher.getId(), null, 200); // Type 정보 누락 + Voucher incompleteVoucher2 = new Voucher(voucher.getId(), new FixedAmountVoucher(200), + null); // BenefitValue 정보 누락 + + // when && then + assertThatThrownBy(() -> voucherRepository.update(incompleteVoucher)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> voucherRepository.update(incompleteVoucher2)) + .isInstanceOf(DataAccessException.class); + } + +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java new file mode 100644 index 0000000000..de89e817be --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java @@ -0,0 +1,40 @@ +package com.programmers.springbootbasic.domain.voucher.presentation.dto; + +import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_VOUCHER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CreateVoucherRequestTest { + + @DisplayName("of 메서드를 통해 적절한 CreateVoucherRequest 객체를 반환한다.") + @Test + void success_of() { + // given + String stringVoucherType = "FIXED"; + Integer benefitValue = 100; + + // when + CreateVoucherRequest request = CreateVoucherRequest.of(stringVoucherType, benefitValue); + + // then + assertThat(request.getVoucherType()).isNotNull(); + assertThat(request.getBenefitValue()).isEqualTo(benefitValue); + } + + @DisplayName("올바르지 않은 VoucherType 문자열로 of 메서드를 호출할 경우 예외를 발생시킨다.") + @Test + void fail_of() { + // given + String invalidStringVoucherType = "INVALID_TYPE"; + Integer benefitValue = 100; + + // then + assertThatThrownBy(() -> CreateVoucherRequest.of(invalidStringVoucherType, benefitValue)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_VOUCHER.getMessage()); + } +} From cae88a43793e3e81065c25694e9e4b5bae627aee Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 05:40:00 +0900 Subject: [PATCH 59/97] =?UTF-8?q?[add]=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=ED=8C=8C=EC=9D=BC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-test.yml | 6 ++++++ src/main/resources/application.yml | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/application-test.yml diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000000..97622732b2 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,6 @@ +spring: + datasource: + url: jdbc:mysql://${TEST_HOST}/spring_basic + username: ${TEST_USERNAME} + password: ${TEST_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 35f15831f0..b7f485382c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,8 +3,13 @@ server: spring: lifecycle: timeout-per-shutdown-phase: 10s + datasource: + url: jdbc:mysql://${HOST}/spring_basic + username: ${USERNAME} + password: ${PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver file: user: path: blacklist.csv voucher: - path: voucher.csv \ No newline at end of file + path: voucher.csv From 47db5f58105ca98377b2885b8311d6bd93902859 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 06:02:21 +0900 Subject: [PATCH 60/97] =?UTF-8?q?[change]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=96=88=EB=8D=98=20=EB=B6=80=EB=B6=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/UserVoucherWalletService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java index 85b41a0dc1..60e0a60dda 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletService.java @@ -52,9 +52,8 @@ public Long create(CreateUserVoucherWalletRequest request) { .orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); var nowTime = timeGenerator.now(); - var test = request.toEntity(user.getId(), nowTime); - var a = userVoucherWalletRepository.save(request.toEntity(user.getId(), nowTime)); - return a.getId(); + var result = userVoucherWalletRepository.save(request.toEntity(user.getId(), nowTime)); + return result.getId(); } public List findUserByVoucherId(UUID voucherId) { From 2031843c100ba80f552c053f18b98ff630e0d709 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 29 Oct 2023 07:08:35 +0900 Subject: [PATCH 61/97] =?UTF-8?q?[feat]=20=EC=9C=A0=EC=A0=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/UserService.java | 14 ++++++++++ .../user/presentation/UserController.java | 5 ++++ .../presentation/dto/CreateUserRequest.java | 25 +++++++++++++++++ .../springbootbasic/exception/ErrorCode.java | 1 + .../IO/ConsoleInteractionAggregator.java | 6 +++++ .../RegisterUserGenerator.java | 27 +++++++++++++++++++ .../presentation/ControllerAdapter.java | 8 ++++++ .../presentation/MainMenu.java | 1 + .../springbootbasic/util/Messages.java | 2 ++ 9 files changed, 89 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java create mode 100644 src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserGenerator.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java index feaa48264c..52929f3391 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/application/UserService.java @@ -1,7 +1,11 @@ package com.programmers.springbootbasic.domain.user.application; +import static com.programmers.springbootbasic.exception.ErrorCode.DUPLICATED_USER; + import com.programmers.springbootbasic.domain.user.domain.UserRepository; +import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import com.programmers.springbootbasic.exception.exceptionClass.UserException; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +22,16 @@ public UserService( this.userRepository = userRepository; } + @Transactional + public Long create(CreateUserRequest request) { + // 유효성 검사 + if (userRepository.findByNickname(request.getNickname()).isPresent()) { + throw new UserException(DUPLICATED_USER); + } + var result = userRepository.save(request.toEntity()); + return result.getId(); + } + public List findBlacklistedUsers() { return userRepository.findBlacklistedUsers() .stream() diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java index 0f84c659bc..117cde4a14 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java @@ -1,6 +1,7 @@ package com.programmers.springbootbasic.domain.user.presentation; import com.programmers.springbootbasic.domain.user.application.UserService; +import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; import java.util.List; import org.springframework.stereotype.Controller; @@ -14,6 +15,10 @@ public UserController(UserService userService) { this.userService = userService; } + public void createUser(CreateUserRequest request) { + userService.create(request); + } + public List getBlackList() { return userService.findBlacklistedUsers(); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java new file mode 100644 index 0000000000..7f4a8caaa7 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java @@ -0,0 +1,25 @@ +package com.programmers.springbootbasic.domain.user.presentation.dto; + +import com.programmers.springbootbasic.domain.user.domain.entity.User; + +public class CreateUserRequest { + + private String nickname; + + public CreateUserRequest(String nickname) { + this.nickname = nickname; + } + + public String getNickname() { + return nickname; + } + + public static CreateUserRequest of(String nickname) { + return new CreateUserRequest(nickname); + } + + public User toEntity() { + return new User(null, nickname, false); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java index 05de3b7b5e..3a06bd88d8 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -15,6 +15,7 @@ public enum ErrorCode { FAIL_TO_DELETE_VOUCHER("바우처 삭제에 실패하였습니다."), FAIL_TO_UPDATE_VOUCHER("바우처 수정에 실패하였습니다."), DUPLICATED_VOUCHER("이미 등록된 바우처 입니다."), + DUPLICATED_USER("이미 등록된 사용자 입니다."), FAIL_TO_DELETE_USER_VOUCHER("사용자 바우처 삭제에 실패하였습니다."), ; diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java index 189b5a31c0..6de7df25e1 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java @@ -1,5 +1,6 @@ package com.programmers.springbootbasic.infrastructure.IO; +import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.mediator.dto.UpdateVoucherMediatorRequest; @@ -26,6 +27,11 @@ public CreateVoucherRequest collectVoucherInput() { return CreateVoucherRequest.of(title, amount); } + public CreateUserRequest collectUserInput() { + String nickname = console.collectStringInput(Messages.INPUT_NICKNAME.getMessage()); + return CreateUserRequest.of(nickname); + } + //TODO: 메세지 어찌할지 public Long collectIdInput() { return console.collectLongInput(Messages.INPUT_ID.getMessage()); diff --git a/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserGenerator.java b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserGenerator.java new file mode 100644 index 0000000000..99e3431ac8 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/mediator/requestGenerator/RegisterUserGenerator.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.mediator.requestGenerator; + +import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; +import com.programmers.springbootbasic.infrastructure.IO.ConsoleInteractionAggregator; +import com.programmers.springbootbasic.mediator.ConsoleRequest; +import com.programmers.springbootbasic.presentation.MainMenu; +import org.springframework.stereotype.Component; + +@Component +public class RegisterUserGenerator implements MenuRequestGenerator{ + + private final ConsoleInteractionAggregator consoleInteractionAggregator; + + public RegisterUserGenerator(ConsoleInteractionAggregator consoleInteractionAggregator) { + this.consoleInteractionAggregator = consoleInteractionAggregator; + } + + @Override + public String getMenuCommand() { + return MainMenu.CREATE_USER.getCommand(); + } + + @Override + public ConsoleRequest generateRequest() { + return new ConsoleRequest<>(getMenuCommand(), consoleInteractionAggregator.collectUserInput()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java index ab4b2e3362..245320645a 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/ControllerAdapter.java @@ -3,6 +3,7 @@ import static com.programmers.springbootbasic.exception.ErrorCode.EXIT; import static com.programmers.springbootbasic.util.Messages.SUCCESS_BLACK_USER_LIST; import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_FOUND_BY_VOUCHER; +import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_REGISTER; import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_VOUCHER_DELETE; import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_VOUCHER_FOUND_MINE; import static com.programmers.springbootbasic.util.Messages.SUCCESS_USER_VOUCHER_REGISTER; @@ -13,6 +14,7 @@ import static com.programmers.springbootbasic.util.Messages.SUCCESS_VOUCHER_UPDATE; import com.programmers.springbootbasic.domain.user.presentation.UserController; +import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.UserVoucherWalletController; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; @@ -78,6 +80,12 @@ public ConsoleResponse updateVoucher(Object... params) { return ConsoleResponse.createNoBodyResponse(SUCCESS_VOUCHER_UPDATE.getMessage()); } + public ConsoleResponse createUser(Object... params) { + CreateUserRequest request = (CreateUserRequest) params[0]; + userController.createUser(request); + return ConsoleResponse.createNoBodyResponse(SUCCESS_USER_REGISTER.getMessage()); + } + public ConsoleResponse> getBlackList(Object... params) { return ConsoleResponse.createWithBodyResponse(userController.getBlackList(), SUCCESS_BLACK_USER_LIST.getMessage()); diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index bb55a187c1..f7d9e054be 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -15,6 +15,7 @@ public enum MainMenu { FIND_VOUCHER("find voucher", ControllerAdapter::getVoucherById), DELETE_VOUCHER("delete voucher", ControllerAdapter::deleteVoucherById), UPDATE_VOUCHER("update voucher", ControllerAdapter::updateVoucher), + CREATE_USER("register user", ControllerAdapter::createUser), LIST_BLACK_USER("list black user", ControllerAdapter::getBlackList), CREATE_USER_VOUCHER("register my voucher", ControllerAdapter::createUserVoucher), FIND_USER_BY_VOUCHER("find users by voucher Id", ControllerAdapter::findUserByVoucherId), diff --git a/src/main/java/com/programmers/springbootbasic/util/Messages.java b/src/main/java/com/programmers/springbootbasic/util/Messages.java index 097da7aa89..9383e604a1 100644 --- a/src/main/java/com/programmers/springbootbasic/util/Messages.java +++ b/src/main/java/com/programmers/springbootbasic/util/Messages.java @@ -13,6 +13,7 @@ public enum Messages { 바우처 수정 : update voucher === User Menu === + 유저 등록 : register user 유저 블랙리스트 조회 : list black user 내 앞으로 바우처 등록 : register my voucher 내 바우처 조회 : find my voucher @@ -35,6 +36,7 @@ public enum Messages { SUCCESS_USER_FOUND_BY_VOUCHER("요청하신 바우처를 소유한 유저 목록입니다."), SUCCESS_USER_VOUCHER_FOUND_MINE("소유하신 바우처 목록입니다."), SUCCESS_USER_VOUCHER_DELETE("요청하신 바우처가 삭제되었습니다."), + SUCCESS_USER_REGISTER("사용자가 등록되었습니다."), ; Messages(String Message) { From c3e314e77fca7b12bd9a39ebe9788ec03424d3e9 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:22:53 +0900 Subject: [PATCH 62/97] =?UTF-8?q?[fix]=20=ED=95=84=EB=93=9C=20name=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/application/UserServiceTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java index 9be4532484..af2455dfff 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java @@ -1,6 +1,7 @@ package com.programmers.springbootbasic.domain.user.application; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.mockito.Mockito.when; import com.programmers.springbootbasic.domain.user.domain.UserRepository; @@ -41,7 +42,7 @@ void success_findBlacklistedUsers() { // then assertThat(result).hasSize(1); assertThat(result) - .extracting("name") - .containsExactlyInAnyOrder("user1"); + .extracting("nickname", "blocked") + .containsExactlyInAnyOrder(tuple("user1", true)); } } From b7696d23051d5c9a5e5cd4837d74745e8f4ecc39 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 03:55:19 +0900 Subject: [PATCH 63/97] =?UTF-8?q?[add]=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20delete,=20put=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../programmers/springbootbasic/AppConfig.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/AppConfig.java diff --git a/src/main/java/com/programmers/springbootbasic/AppConfig.java b/src/main/java/com/programmers/springbootbasic/AppConfig.java new file mode 100644 index 0000000000..31c9dc5a5a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/AppConfig.java @@ -0,0 +1,18 @@ +package com.programmers.springbootbasic; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.filter.HiddenHttpMethodFilter; +@Profile(value = "tomcat") +@Configuration +public class AppConfig { + + @Bean + public FilterRegistrationBean hiddenHttpMethodFilter() { + FilterRegistrationBean filterRegBean = new FilterRegistrationBean<>(); + filterRegBean.setFilter(new HiddenHttpMethodFilter()); + return filterRegBean; + } +} From 36253f728fcd08c63760bfc28ab327440b5bac29 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 03:56:03 +0900 Subject: [PATCH 64/97] =?UTF-8?q?[change]=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?private?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/presentation/dto/CreateUserRequest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java index 7f4a8caaa7..49ce16fa04 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java @@ -2,11 +2,12 @@ import com.programmers.springbootbasic.domain.user.domain.entity.User; + public class CreateUserRequest { private String nickname; - public CreateUserRequest(String nickname) { + private CreateUserRequest(String nickname) { this.nickname = nickname; } From 126b4af472a5e618e6e6b00691693dae772c810d Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 03:57:31 +0900 Subject: [PATCH 65/97] =?UTF-8?q?[feat]=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/UserThymeController.java | 43 ++++ .../UserVoucherWalletThymeController.java | 84 ++++++++ .../presentation/VoucherThymeController.java | 82 +++++++ src/main/resources/static/hug.png | Bin 0 -> 11765 bytes src/main/resources/templates/customError.html | 24 +++ .../resources/templates/defaultError.html | 24 +++ src/main/resources/templates/home.html | 168 +++++++++++++++ .../templates/user/blacklistedUsers.html | 143 +++++++++++++ .../resources/templates/user/register.html | 134 ++++++++++++ .../templates/user/registerationComplete.html | 142 +++++++++++++ .../templates/userVoucher/deleteComplete.html | 16 ++ .../userVoucher/enterUserNickname.html | 138 ++++++++++++ .../templates/userVoucher/enterVoucherId.html | 201 ++++++++++++++++++ .../templates/userVoucher/register.html | 161 ++++++++++++++ .../userVoucher/registerationComplete.html | 139 ++++++++++++ .../userVoucher/userByVoucherIdResult.html | 142 +++++++++++++ .../voucherByUserNicknameResult.html | 179 ++++++++++++++++ .../templates/voucher/deleteComplete.html | 16 ++ .../resources/templates/voucher/details.html | 134 ++++++++++++ .../resources/templates/voucher/list.html | 161 ++++++++++++++ .../resources/templates/voucher/register.html | 172 +++++++++++++++ .../voucher/registerationComplete.html | 145 +++++++++++++ .../resources/templates/voucher/update.html | 173 +++++++++++++++ .../templates/voucher/updateComplete.html | 147 +++++++++++++ .../dto/CreateVoucherRequestTest.java | 13 +- 25 files changed, 2776 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java create mode 100644 src/main/resources/static/hug.png create mode 100644 src/main/resources/templates/customError.html create mode 100644 src/main/resources/templates/defaultError.html create mode 100644 src/main/resources/templates/home.html create mode 100644 src/main/resources/templates/user/blacklistedUsers.html create mode 100644 src/main/resources/templates/user/register.html create mode 100644 src/main/resources/templates/user/registerationComplete.html create mode 100644 src/main/resources/templates/userVoucher/deleteComplete.html create mode 100644 src/main/resources/templates/userVoucher/enterUserNickname.html create mode 100644 src/main/resources/templates/userVoucher/enterVoucherId.html create mode 100644 src/main/resources/templates/userVoucher/register.html create mode 100644 src/main/resources/templates/userVoucher/registerationComplete.html create mode 100644 src/main/resources/templates/userVoucher/userByVoucherIdResult.html create mode 100644 src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html create mode 100644 src/main/resources/templates/voucher/deleteComplete.html create mode 100644 src/main/resources/templates/voucher/details.html create mode 100644 src/main/resources/templates/voucher/list.html create mode 100644 src/main/resources/templates/voucher/register.html create mode 100644 src/main/resources/templates/voucher/registerationComplete.html create mode 100644 src/main/resources/templates/voucher/update.html create mode 100644 src/main/resources/templates/voucher/updateComplete.html diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java new file mode 100644 index 0000000000..05c5b7539f --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java @@ -0,0 +1,43 @@ +package com.programmers.springbootbasic.domain.user.presentation; + +import com.programmers.springbootbasic.domain.user.application.UserService; +import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/user") +public class UserThymeController { + + private final UserService userService; + + public UserThymeController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/register") + public String getRegisterPage() { + return "user/register"; + } + + @PostMapping("/register") + public String createUser(@ModelAttribute CreateUserRequest request, Model model) { + Long id = userService.create(request); + + model.addAttribute("userNickname", request.getNickname()); + model.addAttribute("userId", id); + + return "user/registerationComplete"; + } + + @GetMapping + public String getBlackList(Model model) { + var blacklistedUsers = userService.findBlacklistedUsers(); + model.addAttribute("blacklistedUsers", blacklistedUsers); + return "user/blacklistedUsers"; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java new file mode 100644 index 0000000000..bac2ed97ce --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java @@ -0,0 +1,84 @@ +package com.programmers.springbootbasic.domain.userVoucherWallet.presentation; + +import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import com.programmers.springbootbasic.domain.userVoucherWallet.application.UserVoucherWalletService; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.UserOwnedVoucherResponse; +import com.programmers.springbootbasic.domain.voucher.application.VoucherService; +import java.util.List; +import java.util.UUID; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/userVoucher") +public class UserVoucherWalletThymeController { + + private final UserVoucherWalletService userVoucherWalletService; + private final VoucherService voucherService; + + public UserVoucherWalletThymeController( + UserVoucherWalletService userVoucherWalletService, + VoucherService voucherService + ) { + this.userVoucherWalletService = userVoucherWalletService; + this.voucherService = voucherService; + } + + @GetMapping("/register") + public String getRegisterPage(Model model) { + var vouchers = voucherService.findAll(); + model.addAttribute("vouchers", vouchers); + return "userVoucher/register"; + } + + @PostMapping("/register") + public String registerUserVoucher( + @ModelAttribute CreateUserVoucherWalletRequest request, Model model + ) { + Long id = userVoucherWalletService.create(request); + model.addAttribute("id", id); + return "userVoucher/registerationComplete"; + } + + @PostMapping("/delete/{id}") + public String deleteUserVoucher(@PathVariable Long id) { + userVoucherWalletService.deleteById(id); + return "userVoucher/deleteComplete"; + } + + @GetMapping("/userByVoucher") + public String getUserByVoucherIdPage(Model model) { + var vouchers = voucherService.findAll(); + model.addAttribute("vouchers", vouchers); + return "userVoucher/enterVoucherId"; + } + + @GetMapping("/userByVoucher/{voucherId}") + public String getUserByVoucherId(@PathVariable UUID voucherId, Model model) { + List users = userVoucherWalletService.findUserByVoucherId(voucherId); + model.addAttribute("users", users); + return "userVoucher/userByVoucherIdResult"; + } + + @GetMapping("/voucherByUser") + public String getVoucherByUserNicknamePage() { + return "userVoucher/enterUserNickname"; + } + + @GetMapping("/voucherByUser/{userNickname}") + public String getVoucherByUserNickname( + @PathVariable String userNickname, Model model + ) { + List vouchers = userVoucherWalletService.findVoucherByUserNickname( + userNickname); + model.addAttribute("vouchers", vouchers); + return "userVoucher/voucherByUserNicknameResult"; + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java new file mode 100644 index 0000000000..65e4b00a27 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java @@ -0,0 +1,82 @@ +package com.programmers.springbootbasic.domain.voucher.presentation; + +import com.programmers.springbootbasic.domain.voucher.application.VoucherService; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import java.util.List; +import java.util.UUID; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RequestMapping; + +@Profile(value = "tomcat") +@Controller +@RequestMapping("/voucher") +public class VoucherThymeController { + + private final VoucherService voucherService; + + public VoucherThymeController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + @GetMapping("/register") + public String getVoucherPage() { + return "voucher/register"; + } + + @PostMapping("/register") + public String createVoucher(@ModelAttribute CreateVoucherRequest request, Model model) { + voucherService.create(request); + model.addAttribute("voucherType", request.getVoucherType().getVoucherTypeName()); + model.addAttribute("benefit", request.getBenefitValue()); + + return "voucher/registerationComplete"; + } + + @GetMapping("/vouchers") + public String getAllVouchers(Model model) { + List vouchers = voucherService.findAll(); + model.addAttribute("vouchers", vouchers); + return "voucher/list"; + } + + @GetMapping("/{id}") + public String getVoucherDetails(@PathVariable UUID id, Model model) { + VoucherResponse voucher = voucherService.findById(id); + model.addAttribute("voucher", voucher); + return "voucher/details"; + } + + @GetMapping("/update/{id}") + public String getUpdateVoucherPage(@PathVariable UUID id, Model model) { + VoucherResponse voucher = voucherService.findById(id); + model.addAttribute("id", id); + return "voucher/update"; + } + + @PutMapping("/update/{id}") + public String updateVoucher( + @PathVariable UUID id, @ModelAttribute UpdateVoucherRequest request, Model model + ) { + voucherService.update(id, request); + model.addAttribute("voucherType", request.getVoucherType().getVoucherTypeName()); + model.addAttribute("benefit", request.getBenefitValue()); + return "voucher/updateComplete"; + } + + @DeleteMapping("/delete/{id}") + public String getDeleteVoucher(@PathVariable UUID id) { + voucherService.deleteById(id); + return "voucher/deleteComplete"; + } + +} diff --git a/src/main/resources/static/hug.png b/src/main/resources/static/hug.png new file mode 100644 index 0000000000000000000000000000000000000000..960b3cf5cc3b4f2502f9bfea17e3dfc58cceea9e GIT binary patch literal 11765 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 R4NefjoY&VVJ!>2Ke*iO~*2Dk+ literal 0 HcmV?d00001 diff --git a/src/main/resources/templates/customError.html b/src/main/resources/templates/customError.html new file mode 100644 index 0000000000..9c00893d58 --- /dev/null +++ b/src/main/resources/templates/customError.html @@ -0,0 +1,24 @@ + + + + + Error + + + + + +
+
+

+

+

Sorry, something is wrong.

+ +
+
+ + + diff --git a/src/main/resources/templates/defaultError.html b/src/main/resources/templates/defaultError.html new file mode 100644 index 0000000000..0c366b8d3d --- /dev/null +++ b/src/main/resources/templates/defaultError.html @@ -0,0 +1,24 @@ + + + + + Error + + + + + +
+
+

500

+

Error

+

Sorry, something is wrong.

+ +
+
+ + + diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000000..ac0bbecb43 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,168 @@ + + + + + Home + + + + + + +
+ + + +
+
+ + + + + + + + diff --git a/src/main/resources/templates/user/blacklistedUsers.html b/src/main/resources/templates/user/blacklistedUsers.html new file mode 100644 index 0000000000..f8b47582f1 --- /dev/null +++ b/src/main/resources/templates/user/blacklistedUsers.html @@ -0,0 +1,143 @@ + + + + + Blacklisted Users + + + + + +
+

User Blacklist

+
+ + + + + + + + + + + + +
User nickname
+
+ +
+

No users are currently blacklisted.

+
+
+ + + + + diff --git a/src/main/resources/templates/user/register.html b/src/main/resources/templates/user/register.html new file mode 100644 index 0000000000..368e2be487 --- /dev/null +++ b/src/main/resources/templates/user/register.html @@ -0,0 +1,134 @@ + + + + + Register + + + + +

Sign up

+
+
+ +
+ + + + +
+ +
+ + + + + diff --git a/src/main/resources/templates/user/registerationComplete.html b/src/main/resources/templates/user/registerationComplete.html new file mode 100644 index 0000000000..1fab41c25f --- /dev/null +++ b/src/main/resources/templates/user/registerationComplete.html @@ -0,0 +1,142 @@ + + + + + Registration Complete + + + + + +
+
+
+

유저 등록 완료

+
+
+
+
+
User ID
+
UserID123
+
+
+
User Nickname
+
UserNickname
+
+
+
+
+ +

감사합니다! 사용자 등록이 완료되었습니다.

+
+ + + + + diff --git a/src/main/resources/templates/userVoucher/deleteComplete.html b/src/main/resources/templates/userVoucher/deleteComplete.html new file mode 100644 index 0000000000..a62085a1d3 --- /dev/null +++ b/src/main/resources/templates/userVoucher/deleteComplete.html @@ -0,0 +1,16 @@ + + + + + + Delete Complete + + + +
+

삭제 완료!

+

유저가 소유한 바우처 삭제가 완료되었습니다.

+ Back to Home +
+ + diff --git a/src/main/resources/templates/userVoucher/enterUserNickname.html b/src/main/resources/templates/userVoucher/enterUserNickname.html new file mode 100644 index 0000000000..b77f7def10 --- /dev/null +++ b/src/main/resources/templates/userVoucher/enterUserNickname.html @@ -0,0 +1,138 @@ + + + + + User Nickname을 입력하세요 + + + + + +
+

Nickname 을 입력하세요

+

해당 닉네임을 가진 유저의 바우처를 조회 할 수 있습니다.

+
+ + +
+
+ +
+
+ + + + diff --git a/src/main/resources/templates/userVoucher/enterVoucherId.html b/src/main/resources/templates/userVoucher/enterVoucherId.html new file mode 100644 index 0000000000..6aed5d7744 --- /dev/null +++ b/src/main/resources/templates/userVoucher/enterVoucherId.html @@ -0,0 +1,201 @@ + + + + + Voucher ID 를 선택하세요 + + + + + + +
+

Voucher ID 를 선택하세요

+

해당 Voucher ID를 가진 유저들을 조회 할 수 있습니다.

+
+ + + + + + + + + + + + + + + + +
Voucher IDTypeBenefit
+ +
+
+
+ +
+
+ + + diff --git a/src/main/resources/templates/userVoucher/register.html b/src/main/resources/templates/userVoucher/register.html new file mode 100644 index 0000000000..e1b9d7a6bc --- /dev/null +++ b/src/main/resources/templates/userVoucher/register.html @@ -0,0 +1,161 @@ + + + + Register Voucher + + + + + + +
+

Register Voucher

+

내 소유의 Voucher를 등록하세요.

+
+
+ + +
+
+ Available Vouchers: +
+ + +
+
+
+ +
+
+
+ + + + diff --git a/src/main/resources/templates/userVoucher/registerationComplete.html b/src/main/resources/templates/userVoucher/registerationComplete.html new file mode 100644 index 0000000000..c74be4dec7 --- /dev/null +++ b/src/main/resources/templates/userVoucher/registerationComplete.html @@ -0,0 +1,139 @@ + + + + + Registration Complete + + + + + +
+
+
+

바우처 등록 완료

+
+
+
+
+
User Voucher ID
+
123123
+
+
+
+
+ +

감사합니다! 바우처 등록이 완료되었습니다.

+
+ + + + + + diff --git a/src/main/resources/templates/userVoucher/userByVoucherIdResult.html b/src/main/resources/templates/userVoucher/userByVoucherIdResult.html new file mode 100644 index 0000000000..4a39aa79ac --- /dev/null +++ b/src/main/resources/templates/userVoucher/userByVoucherIdResult.html @@ -0,0 +1,142 @@ + + + + + Find User By Voucher ID + + + + + +
+

User List

+

해당 바우처를 가지고 있는 유저 목록입니다.

+
+ + + + + + + + + + + + +
User nickname
+
+ +
+

No users are currently has Voucher.

+
+
+ + + diff --git a/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html b/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html new file mode 100644 index 0000000000..8e8a7e3f51 --- /dev/null +++ b/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html @@ -0,0 +1,179 @@ + + + + + Vouchers of User + + + + + + + +
+

Vouchers of

+

해당 유저가 가지고 있는 바우처 목록입니다.

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ User Voucher Id + + Voucher UUID + + Voucher Type + + Benefit + + Details + + Del +
Voucher IDVoucher UUIDVoucher TypeBenefit + View + +
+ + +
+
+
+ +
+

No Vouchers.

+
+
+ + + diff --git a/src/main/resources/templates/voucher/deleteComplete.html b/src/main/resources/templates/voucher/deleteComplete.html new file mode 100644 index 0000000000..7239a5bfec --- /dev/null +++ b/src/main/resources/templates/voucher/deleteComplete.html @@ -0,0 +1,16 @@ + + + + + + Delete Complete + + + +
+

삭제 완료!

+

바우처 삭제가 완료되었습니다.

+ Back to Home +
+ + diff --git a/src/main/resources/templates/voucher/details.html b/src/main/resources/templates/voucher/details.html new file mode 100644 index 0000000000..3838cd9ea4 --- /dev/null +++ b/src/main/resources/templates/voucher/details.html @@ -0,0 +1,134 @@ + + + + + + + Voucher Details + + + +
+
+
+

Voucher Details

+

Voucher Type:

+

Benefit:

+

Created At:

+ +
+ Edit +
+
+ + + + + + diff --git a/src/main/resources/templates/voucher/list.html b/src/main/resources/templates/voucher/list.html new file mode 100644 index 0000000000..c1a7e3c5c2 --- /dev/null +++ b/src/main/resources/templates/voucher/list.html @@ -0,0 +1,161 @@ + + + + + + Vouchers + + + + + +
+

Voucher List

+
+ + + + + + + + + + + + + + + + + + + + +
IDVoucherTypeBenefitActions
+ View + Edit +
+ +
+
+
+
+ + +
+

No Vouchers.

+
+
+ + + + diff --git a/src/main/resources/templates/voucher/register.html b/src/main/resources/templates/voucher/register.html new file mode 100644 index 0000000000..f1afb57826 --- /dev/null +++ b/src/main/resources/templates/voucher/register.html @@ -0,0 +1,172 @@ + + + + Create Voucher + + + + + + + + +
+
+
+
+

Create New Voucher

+

새로운 Voucher를 등록해보세요.

+
+
+ +
+ +
+ + + +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ + + diff --git a/src/main/resources/templates/voucher/registerationComplete.html b/src/main/resources/templates/voucher/registerationComplete.html new file mode 100644 index 0000000000..5d614646d7 --- /dev/null +++ b/src/main/resources/templates/voucher/registerationComplete.html @@ -0,0 +1,145 @@ + + + + + Registration Complete + + + + + +
+
+
+

바우처 등록 완료

+
+
+
+
+
Voucher Type
+
123123
+
+
+
+
+
Benefit
+
123123
+
+
+
+
+ +

감사합니다! 바우처 등록이 완료되었습니다.

+
+ + + + + + diff --git a/src/main/resources/templates/voucher/update.html b/src/main/resources/templates/voucher/update.html new file mode 100644 index 0000000000..dd78d427fe --- /dev/null +++ b/src/main/resources/templates/voucher/update.html @@ -0,0 +1,173 @@ + + + + Edit Voucher + + + + + + + + +
+
+
+
+

Edit Voucher

+

바우처 "" 를 수정합니다.

+
+
+ +
+ +
+ +
+
+
+
+ + +
+ +
+ +
+
+
+
+
+
+ + + + + + diff --git a/src/main/resources/templates/voucher/updateComplete.html b/src/main/resources/templates/voucher/updateComplete.html new file mode 100644 index 0000000000..35909d14dd --- /dev/null +++ b/src/main/resources/templates/voucher/updateComplete.html @@ -0,0 +1,147 @@ + + + + + Registration Complete + + + + + +
+
+
+

업데이트 완료

+
+
+
+
+
Voucher Type
+
123123
+
+
+
+
+
Benefit
+
123123
+
+
+
+
+ +

성공적으로 바우처 정보가 업데이트되었습니다.

+
+ + + + + + + + diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java index de89e817be..ebeae89055 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java @@ -4,7 +4,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import com.programmers.springbootbasic.exception.exceptionClass.MenuException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,11 +15,11 @@ class CreateVoucherRequestTest { @Test void success_of() { // given - String stringVoucherType = "FIXED"; + VoucherTypeEnum voucherType = VoucherTypeEnum.FIXED; Integer benefitValue = 100; // when - CreateVoucherRequest request = CreateVoucherRequest.of(stringVoucherType, benefitValue); + CreateVoucherRequest request = CreateVoucherRequest.of(voucherType, benefitValue); // then assertThat(request.getVoucherType()).isNotNull(); @@ -33,8 +34,10 @@ void fail_of() { Integer benefitValue = 100; // then - assertThatThrownBy(() -> CreateVoucherRequest.of(invalidStringVoucherType, benefitValue)) - .isInstanceOf(CustomException.class) + assertThatThrownBy( + () -> CreateVoucherRequest.of(VoucherTypeEnum.of(invalidStringVoucherType), + benefitValue)) + .isInstanceOf(MenuException.class) .hasMessageContaining(INVALID_VOUCHER.getMessage()); } } From 79d375216d6ca782a7f863a4056edf6b148eb439 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 03:58:57 +0900 Subject: [PATCH 66/97] =?UTF-8?q?[change]=20sql=20time=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EA=B3=B5=ED=86=B5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JdbcUserVoucherWalletRepository.java | 13 ++++--------- .../infrastructure/JdbcVoucherRepository.java | 13 +++++++++---- .../springbootbasic/util/SqlConverter.java | 13 +++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/util/SqlConverter.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java index dee6732c6d..1af8f0215e 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepository.java @@ -7,9 +7,8 @@ import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithUser; import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.UserVoucherWalletWithVoucher; +import com.programmers.springbootbasic.util.SqlConverter; import java.sql.PreparedStatement; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -29,7 +28,7 @@ public class JdbcUserVoucherWalletRepository implements UserVoucherWalletReposit rs.getLong("id"), rs.getLong("user_id"), UUID.fromString(rs.getString("voucher_id")), - toLocalDateTime(rs.getString("created_at")) + SqlConverter.toLocalDateTime(rs.getString("created_at")) ); public JdbcUserVoucherWalletRepository(JdbcTemplate jdbcTemplate) { @@ -94,7 +93,8 @@ public List findVoucherByUserId(Long userId) { UUID.fromString(rs.getString("voucher_id")), VoucherTypeEnum.of(rs.getString("voucher_type")) .getVoucherType(rs.getInt("benefit_value")), - rs.getInt("benefit_value") + rs.getInt("benefit_value"), + SqlConverter.toLocalDateTime(rs.getString("created_at")) ) ); @@ -126,9 +126,4 @@ public List findUserByVoucherId(UUID voucherId) { """, voucherWalletJoinRowMapper, voucherId.toString()); } - - private LocalDateTime toLocalDateTime(String dateTime) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - return LocalDateTime.parse(dateTime, formatter); - } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java index 84a9988e02..aa6ef9d4b8 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java @@ -3,6 +3,8 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; +import com.programmers.springbootbasic.util.SqlConverter; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -17,6 +19,7 @@ public class JdbcVoucherRepository implements VoucherRepository { private static final String ID = "id"; private static final String VOUCHER_TYPE = "voucher_type"; private static final String BENEFIT_VALUE = "benefit_value"; + private static final String CREATED_AT = "created_at"; private final JdbcTemplate jdbcTemplate; private final RowMapper voucherRowMapper = @@ -24,7 +27,8 @@ public class JdbcVoucherRepository implements VoucherRepository { UUID.fromString(rs.getString(ID)), VoucherTypeEnum.of(rs.getString(VOUCHER_TYPE)) .getVoucherType(rs.getInt(BENEFIT_VALUE)), - rs.getInt(BENEFIT_VALUE) + rs.getInt(BENEFIT_VALUE), + SqlConverter.toLocalDateTime(rs.getString(CREATED_AT)) ); public JdbcVoucherRepository(JdbcTemplate jdbcTemplate) { @@ -34,11 +38,12 @@ public JdbcVoucherRepository(JdbcTemplate jdbcTemplate) { @Override public Voucher save(Voucher voucher) { jdbcTemplate.update( - String.format("INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)", TABLE_NAME, ID, - VOUCHER_TYPE, BENEFIT_VALUE), + String.format("INSERT INTO %s (%s, %s, %s, %s) VALUES (?, ?, ?, ?)", TABLE_NAME, ID, + VOUCHER_TYPE, BENEFIT_VALUE, CREATED_AT), voucher.getId().toString(), voucher.getVoucherType().getVoucherTypeName(), - voucher.getBenefitValue()); + voucher.getBenefitValue(), + voucher.getCreatedAt().toString()); return voucher; } diff --git a/src/main/java/com/programmers/springbootbasic/util/SqlConverter.java b/src/main/java/com/programmers/springbootbasic/util/SqlConverter.java new file mode 100644 index 0000000000..d074dbd2c1 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/util/SqlConverter.java @@ -0,0 +1,13 @@ +package com.programmers.springbootbasic.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class SqlConverter { + + public static LocalDateTime toLocalDateTime(String sqlDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return LocalDateTime.parse(sqlDateTime, formatter); + } + +} From 24623cbbd9be8ba378fe4d2d65a9bbc404761cfe Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:00:22 +0900 Subject: [PATCH 67/97] =?UTF-8?q?[change]=20error=20=EC=83=81=EC=86=8D=20?= =?UTF-8?q?=EA=B4=80=EA=B3=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/AppExceptionHandler.java | 4 +- .../springbootbasic/exception/ErrorCode.java | 42 +++++++++++-------- .../exception/GlobalExceptionHandler.java | 19 +++++++++ .../exception/GlobalRestException.java | 27 ++++++++++++ .../exceptionClass/CustomException.java | 6 +++ .../exceptionClass/FileIOException.java | 5 +-- .../exceptionClass/MenuException.java | 9 ++++ .../exceptionClass/SystemException.java | 11 +---- .../exceptionClass/UserException.java | 5 +-- .../UserVoucherWalletException.java | 5 +-- .../exceptionClass/VoucherException.java | 5 +-- .../provider/MenuRequestProvider.java | 4 +- .../presentation/MainMenu.java | 4 +- .../springbootbasic/util/CsvManager.java | 3 +- 14 files changed, 103 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java create mode 100644 src/main/java/com/programmers/springbootbasic/exception/exceptionClass/MenuException.java diff --git a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java index 6abee48ccd..dfafcbf20b 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/AppExceptionHandler.java @@ -2,7 +2,7 @@ import static com.programmers.springbootbasic.exception.ErrorCode.DATABASE_ERROR; -import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.exception.exceptionClass.MenuException; import com.programmers.springbootbasic.exception.exceptionClass.FileIOException; import com.programmers.springbootbasic.exception.exceptionClass.SystemException; import com.programmers.springbootbasic.exception.exceptionClass.UserException; @@ -44,7 +44,7 @@ public void handle() { requestProcessor.sendResponse( ConsoleResponse.createNoBodyResponse(e.getMessage()) ); - } catch (CustomException e) { + } catch (MenuException e) { logger.error(String.format("Custom Error : %s", e.getMessage())); requestProcessor.sendResponse( ConsoleResponse.createNoBodyResponse(e.getMessage()) diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java index 3a06bd88d8..43feaf2686 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -1,31 +1,39 @@ package com.programmers.springbootbasic.exception; +import org.springframework.http.HttpStatus; + public enum ErrorCode { - FILE_IO_ERROR("파일 에러입니다. 시스템을 종료합니다."), - DATABASE_ERROR("데이터베이스 에러입니다. 저장에 실패하였습니다. 다시 시도하세요."), - INVALID_MENU("올바르지 않은 메뉴 타입입니다."), - INVALID_VOUCHER("올바르지 않은 바우처 타입 입니다. 현재 등록 가능한 바우처 타입은 FIXED, PERCENT 입니다."), - INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다."), - INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다."), - INVALID_FILE_PATH("올바르지 않은 파일 경로입니다."), - EXIT("시스템을 종료합니다."), - NOT_FOUND_VOUCHER("바우처를 찾을 수 없습니다."), - NOT_FOUND_USER("사용자를 찾을 수 없습니다."), - NOT_FOUND_USER_VOUCHER("사용자 바우처를 찾을 수 없습니다."), - FAIL_TO_DELETE_VOUCHER("바우처 삭제에 실패하였습니다."), - FAIL_TO_UPDATE_VOUCHER("바우처 수정에 실패하였습니다."), - DUPLICATED_VOUCHER("이미 등록된 바우처 입니다."), - DUPLICATED_USER("이미 등록된 사용자 입니다."), - FAIL_TO_DELETE_USER_VOUCHER("사용자 바우처 삭제에 실패하였습니다."), + FILE_IO_ERROR("파일 에러입니다. 시스템을 종료합니다.", HttpStatus.INTERNAL_SERVER_ERROR), + DATABASE_ERROR("데이터베이스 에러입니다. 저장에 실패하였습니다. 다시 시도하세요.", HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_MENU("올바르지 않은 메뉴 타입입니다.", HttpStatus.BAD_REQUEST), + INVALID_VOUCHER("올바르지 않은 바우처 타입 입니다. 현재 등록 가능한 바우처 타입은 FIXED, PERCENT 입니다.", HttpStatus.BAD_REQUEST), + INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다.", HttpStatus.BAD_REQUEST), + INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다.", HttpStatus.BAD_REQUEST), + INVALID_FILE_PATH("올바르지 않은 파일 경로입니다.", HttpStatus.INTERNAL_SERVER_ERROR), + EXIT("시스템을 종료합니다.", HttpStatus.INTERNAL_SERVER_ERROR), + NOT_FOUND_VOUCHER("바우처를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), + NOT_FOUND_USER("사용자를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), + NOT_FOUND_USER_VOUCHER("사용자 바우처를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), + FAIL_TO_DELETE_VOUCHER("바우처 삭제에 실패하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + FAIL_TO_UPDATE_VOUCHER("바우처 수정에 실패하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + DUPLICATED_VOUCHER("이미 등록된 바우처 입니다.", HttpStatus.CONFLICT), + DUPLICATED_USER("이미 등록된 사용자 입니다.", HttpStatus.CONFLICT), + FAIL_TO_DELETE_USER_VOUCHER("사용자 바우처 삭제에 실패하였습니다.", HttpStatus.INTERNAL_SERVER_ERROR), ; - ErrorCode(String message) { + ErrorCode(String message, HttpStatus httpStatus) { this.message = message; + this.httpStatus = httpStatus; } private final String message; + private final HttpStatus httpStatus; public String getMessage() { return message; } + + public HttpStatus getHttpStatus() { + return httpStatus; + } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000..c051ced7d7 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java @@ -0,0 +1,19 @@ +package com.programmers.springbootbasic.exception; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler({ + CustomException.class, + }) + public String handleUserException(CustomException ex, Model model) { + model.addAttribute("error", ex.getErrorCode().getHttpStatus()); + model.addAttribute("message", ex.getMessage()); + return "customError"; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java new file mode 100644 index 0000000000..92732b1c00 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.exception; + +import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +public class GlobalRestException { + + @ExceptionHandler({ + CustomException.class, + }) + public ResponseEntity handleUserException(CustomException ex) { + return ResponseEntity + .status(ex.getErrorCode().getHttpStatus()) + .body(ex.getMessage()); + } + + @ExceptionHandler({ + Exception.class, + }) + public ResponseEntity handleException(Exception ex) { + return ResponseEntity + .status(500) + .body(ex.getMessage()); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java index da93bef3bf..694b18183e 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/CustomException.java @@ -3,8 +3,14 @@ import com.programmers.springbootbasic.exception.ErrorCode; public class CustomException extends RuntimeException { + private final ErrorCode errorCode; public CustomException(ErrorCode errorCode) { super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java index 45ca49deb0..b2d6166e05 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/FileIOException.java @@ -2,9 +2,8 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class FileIOException extends RuntimeException { - +public class FileIOException extends CustomException { public FileIOException(ErrorCode errorCode) { - super(errorCode.getMessage()); + super(errorCode); } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/MenuException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/MenuException.java new file mode 100644 index 0000000000..a29be16f5e --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/MenuException.java @@ -0,0 +1,9 @@ +package com.programmers.springbootbasic.exception.exceptionClass; + +import com.programmers.springbootbasic.exception.ErrorCode; + +public class MenuException extends CustomException { + public MenuException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java index 7477588421..1abcee0f75 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/SystemException.java @@ -2,16 +2,9 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class SystemException extends RuntimeException { - - private final ErrorCode errorCode; +public class SystemException extends CustomException { public SystemException(ErrorCode errorCode) { - super(errorCode.getMessage()); - this.errorCode = errorCode; - } - - public ErrorCode getErrorCode() { - return this.errorCode; + super(errorCode); } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java index 5cd5a34215..ab4ff2000c 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserException.java @@ -2,9 +2,8 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class UserException extends RuntimeException { - +public class UserException extends CustomException { public UserException(ErrorCode errorCode) { - super(errorCode.getMessage()); + super(errorCode); } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java index 2ecd7541af..b7ffc1789c 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/UserVoucherWalletException.java @@ -2,9 +2,8 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class UserVoucherWalletException extends RuntimeException { - +public class UserVoucherWalletException extends CustomException { public UserVoucherWalletException(ErrorCode errorCode) { - super(errorCode.getMessage()); + super(errorCode); } } diff --git a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java index e43519efef..e2b8c850ff 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/exceptionClass/VoucherException.java @@ -2,9 +2,8 @@ import com.programmers.springbootbasic.exception.ErrorCode; -public class VoucherException extends RuntimeException { - +public class VoucherException extends CustomException { public VoucherException(ErrorCode errorCode) { - super(errorCode.getMessage()); + super(errorCode); } } diff --git a/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java b/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java index 391b5b64d7..96d955e29b 100644 --- a/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java +++ b/src/main/java/com/programmers/springbootbasic/mediator/provider/MenuRequestProvider.java @@ -2,7 +2,7 @@ import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_MENU; -import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.exception.exceptionClass.MenuException; import com.programmers.springbootbasic.mediator.ConsoleRequest; import com.programmers.springbootbasic.mediator.requestGenerator.MenuRequestGenerator; import java.util.List; @@ -22,6 +22,6 @@ public ConsoleRequest getMenuRequest(String menuName) { .filter(menuRequestGenerator -> menuRequestGenerator.getMenuCommand().equals(menuName)) .findFirst() .map(MenuRequestGenerator::generateRequest) - .orElseThrow(() -> new CustomException(INVALID_MENU)); + .orElseThrow(() -> new MenuException(INVALID_MENU)); } } diff --git a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java index f7d9e054be..a9825404e1 100644 --- a/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java +++ b/src/main/java/com/programmers/springbootbasic/presentation/MainMenu.java @@ -2,7 +2,7 @@ import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_MENU; -import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.exception.exceptionClass.MenuException; import com.programmers.springbootbasic.mediator.ConsoleRequest; import com.programmers.springbootbasic.mediator.ConsoleResponse; import java.util.function.BiFunction; @@ -53,6 +53,6 @@ public static ConsoleResponse routeToController( return menuCommand.execute(controllerAdapter); } }) - .orElseThrow(() -> new CustomException(INVALID_MENU)); + .orElseThrow(() -> new MenuException(INVALID_MENU)); } } diff --git a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java index 0dcb11889a..259edf9c6c 100644 --- a/src/main/java/com/programmers/springbootbasic/util/CsvManager.java +++ b/src/main/java/com/programmers/springbootbasic/util/CsvManager.java @@ -3,7 +3,6 @@ import static com.programmers.springbootbasic.exception.ErrorCode.FILE_IO_ERROR; -import com.programmers.springbootbasic.exception.exceptionClass.CustomException; import com.programmers.springbootbasic.exception.exceptionClass.FileIOException; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -47,7 +46,7 @@ public List read(String fileName, Class type) { resultList.add(obj); } } catch (IOException | ReflectiveOperationException e) { - throw new CustomException(FILE_IO_ERROR); + throw new FileIOException(FILE_IO_ERROR); } return resultList; From 97ef4bb334424914f1354c8569f05b9420a70a96 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:00:45 +0900 Subject: [PATCH 68/97] =?UTF-8?q?[add]=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EB=A9=94=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/thyme/HomeController.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/thyme/HomeController.java diff --git a/src/main/java/com/programmers/springbootbasic/thyme/HomeController.java b/src/main/java/com/programmers/springbootbasic/thyme/HomeController.java new file mode 100644 index 0000000000..e9a3f0e28d --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/thyme/HomeController.java @@ -0,0 +1,16 @@ +package com.programmers.springbootbasic.thyme; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("") +@Controller +public class HomeController { + + @GetMapping("") + public String home() { + return "home"; + } + +} From 399a3af947ee0974b69b3957ba3f8d8e0b69cc2b Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:00:57 +0900 Subject: [PATCH 69/97] =?UTF-8?q?[add]=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thyme/CustomErrorController.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java diff --git a/src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java b/src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java new file mode 100644 index 0000000000..65603348da --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java @@ -0,0 +1,14 @@ +package com.programmers.springbootbasic.thyme; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class CustomErrorController implements ErrorController { + + @RequestMapping("/error") + public String handleError() { + return "defaultError"; + } +} From db64b1fb0c2e125f5882946997b3367631454690 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:01:27 +0900 Subject: [PATCH 70/97] =?UTF-8?q?[change]=20voucher=20type=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EA=B5=AC=EA=B0=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/IO/ConsoleInteractionAggregator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java index 6de7df25e1..24e193d116 100644 --- a/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java +++ b/src/main/java/com/programmers/springbootbasic/infrastructure/IO/ConsoleInteractionAggregator.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.mediator.dto.UpdateVoucherMediatorRequest; import com.programmers.springbootbasic.util.Messages; @@ -22,9 +23,9 @@ public String collectMenuInput() { } public CreateVoucherRequest collectVoucherInput() { - String title = console.collectStringInput(Messages.VOUCHER_REGISTER_TYPE.getMessage()); + String type = console.collectStringInput(Messages.VOUCHER_REGISTER_TYPE.getMessage()); int amount = console.collectIntegerInput(Messages.VOUCHER_REGISTER_AMOUNT.getMessage()); - return CreateVoucherRequest.of(title, amount); + return CreateVoucherRequest.of(VoucherTypeEnum.of(type), amount); } public CreateUserRequest collectUserInput() { From 0c3b526df685a1c2cd77e55dac7cca2e16b1503f Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:02:00 +0900 Subject: [PATCH 71/97] =?UTF-8?q?[add]=20getter=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/presentation/dto/UserResponse.java | 4 ++++ .../presentation/dto/UserOwnedVoucherResponse.java | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java index 1817978cb9..ed47aaffb9 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/UserResponse.java @@ -38,4 +38,8 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(nickname); } + + public String getNickname() { + return nickname; + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java index e3346aa4c2..c46a02d2ee 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/UserOwnedVoucherResponse.java @@ -25,4 +25,12 @@ public String toString() { %s """.formatted(id, voucherResponse); } + + public Long getId() { + return id; + } + + public VoucherResponse getVoucherResponse() { + return voucherResponse; + } } From ad8bd01d8b84bac22cc687068b289117aa1592cd Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:02:33 +0900 Subject: [PATCH 72/97] =?UTF-8?q?[change]=20exception=20class=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/domain/VoucherType/VoucherTypeEnum.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java index 418b53fb92..b1ad1aab8b 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherType/VoucherTypeEnum.java @@ -2,7 +2,7 @@ import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_VOUCHER; -import com.programmers.springbootbasic.exception.exceptionClass.CustomException; +import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; import java.util.function.Function; public enum VoucherTypeEnum { @@ -23,7 +23,7 @@ public static VoucherTypeEnum of(String voucherType) { try { return VoucherTypeEnum.valueOf(voucherType.toUpperCase()); } catch (IllegalArgumentException e) { - throw new CustomException(INVALID_VOUCHER); + throw new VoucherException(INVALID_VOUCHER); } } } From 3b729f9132c30f80a67fe711c22fb340db299ba1 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:03:37 +0900 Subject: [PATCH 73/97] =?UTF-8?q?[feat]=20voucher=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voucher/application/VoucherService.java | 18 ++++++-- .../voucher/domain/VoucherRepository.java | 4 +- .../domain/voucher/domain/entity/Voucher.java | 8 +++- .../FilePersistenceVoucherRepository.java | 6 +++ .../infrastructure/JdbcVoucherRepository.java | 33 +++++++++++++++ .../MemoryVoucherRepository.java | 6 +++ .../infrastructure/dto/CsvVoucher.java | 12 ++++-- .../presentation/dto/VoucherCriteria.java | 42 +++++++++++++++++++ .../presentation/dto/VoucherResponse.java | 13 +++++- 9 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java index ac9ad90f5b..c2868def45 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/application/VoucherService.java @@ -4,11 +4,13 @@ import static com.programmers.springbootbasic.exception.ErrorCode.FAIL_TO_UPDATE_VOUCHER; import static com.programmers.springbootbasic.exception.ErrorCode.NOT_FOUND_VOUCHER; +import com.programmers.springbootbasic.common.TimeGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; import java.util.List; @@ -23,18 +25,21 @@ public class VoucherService { private static final int AFFECTED_ROW_ONE = 1; private final VoucherRepository voucherRepository; private final VoucherIdGenerator idGenerator; + private final TimeGenerator timeGenerator; public VoucherService( VoucherRepository voucherRepository, - VoucherIdGenerator idGenerator + VoucherIdGenerator idGenerator, + TimeGenerator timeGenerator ) { this.voucherRepository = voucherRepository; this.idGenerator = idGenerator; + this.timeGenerator = timeGenerator; } @Transactional public UUID create(CreateVoucherRequest request) { - Voucher voucher = request.toEntity(idGenerator.generate()); + Voucher voucher = request.toEntity(idGenerator.generate(), timeGenerator.now()); voucherRepository.save(voucher); return voucher.getId(); } @@ -65,7 +70,8 @@ public UUID update(UUID id, UpdateVoucherRequest request) { Voucher exsistedVoucher = voucherRepository.findById(id) .orElseThrow(() -> new VoucherException(NOT_FOUND_VOUCHER)); - var updatedVoucher = new Voucher(id, request.getVoucherType(), request.getBenefitValue()); + var updatedVoucher = new Voucher(id, request.getVoucherType(), request.getBenefitValue(), + timeGenerator.now()); if (voucherRepository.update(updatedVoucher) != AFFECTED_ROW_ONE) { throw new VoucherException(FAIL_TO_UPDATE_VOUCHER); @@ -73,4 +79,10 @@ public UUID update(UUID id, UpdateVoucherRequest request) { return updatedVoucher.getId(); } + public List findByCriteria(VoucherCriteria voucherCriteria) { + return voucherRepository.findByCriteria(voucherCriteria).stream() + .map(VoucherResponse::of) + .toList(); + } + } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java index 8a4e7153c3..d67d4b42ad 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/VoucherRepository.java @@ -2,8 +2,10 @@ import com.programmers.springbootbasic.common.Repository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; +import java.util.List; import java.util.UUID; public interface VoucherRepository extends Repository { - + List findByCriteria(VoucherCriteria criteria); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java index 2ce021f11a..deaa7ca268 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/domain/entity/Voucher.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; +import java.time.LocalDateTime; import java.util.Objects; import java.util.UUID; @@ -10,11 +11,13 @@ public class Voucher { private final UUID id; private final VoucherType voucherType; private final Integer benefitValue; + private final LocalDateTime createdAt; - public Voucher(UUID id, VoucherType voucherType, Integer benefitValue) { + public Voucher(UUID id, VoucherType voucherType, Integer benefitValue, LocalDateTime createdAt) { this.id = id; this.voucherType = voucherType; this.benefitValue = benefitValue; + this.createdAt = createdAt; } public UUID getId() { @@ -29,6 +32,9 @@ public Integer getBenefitValue() { return benefitValue; } + public LocalDateTime getCreatedAt() { + return createdAt; + } // 테스트코드를 위해 재정의 @Override public boolean equals(Object o) { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java index a8262edd22..ec0c8ed11d 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/FilePersistenceVoucherRepository.java @@ -5,6 +5,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; import com.programmers.springbootbasic.domain.voucher.infrastructure.dto.CsvVoucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; import com.programmers.springbootbasic.exception.exceptionClass.VoucherException; import com.programmers.springbootbasic.util.FileManager; import java.util.List; @@ -79,4 +80,9 @@ private ConcurrentHashMap getCsvVoucherHashMap() { csvVoucher.toEntity())); return csvVoucherHashMap; } + + @Override + public List findByCriteria(VoucherCriteria criteria) { + return null; + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java index aa6ef9d4b8..c8a11bb419 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepository.java @@ -74,4 +74,37 @@ public int update(Voucher voucher) { voucher.getBenefitValue(), voucher.getId().toString()); } + + @Override + public List findByCriteria(VoucherCriteria criteria) { + String baseSql = String.format("SELECT * FROM %s", TABLE_NAME); + + if (criteria.getStartDate() != null && criteria.getEndDate() != null + && criteria.getType() == null) { + baseSql += String.format(" WHERE %s >= '%s' AND %s < '%s'", + CREATED_AT, + criteria.getStartDate(), + CREATED_AT, + criteria.getEndDate()); + } + + if (criteria.getType() != null && criteria.getStartDate() == null + && criteria.getEndDate() == null) { + baseSql += String.format(" WHERE %s = '%s'", VOUCHER_TYPE, criteria.getType()); + } + + if (criteria.getType() != null && criteria.getStartDate() != null + && criteria.getEndDate() != null) { + baseSql += String.format(" WHERE %s = '%s' AND %s >= '%s' AND %s < '%s'", + VOUCHER_TYPE, + criteria.getType(), + CREATED_AT, + criteria.getStartDate(), + CREATED_AT, + criteria.getEndDate()); + } + + return jdbcTemplate.query(baseSql, voucherRowMapper); + } + } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java index d58439fe4b..e7dccbce5e 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/MemoryVoucherRepository.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -40,4 +41,9 @@ public int deleteById(UUID id) { public int update(Voucher voucher) { return vouchers.put(voucher.getId(), voucher) == null ? 0 : 1; } + + @Override + public List findByCriteria(VoucherCriteria criteria) { + return null; + } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java index fd6289f6fa..f2b470ebdc 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/infrastructure/dto/CsvVoucher.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.util.SqlConverter; import java.util.UUID; public class CsvVoucher { @@ -9,26 +10,31 @@ public class CsvVoucher { private String id; private String voucherType; private String benefitValue; + private String createdAt; public CsvVoucher() { } - public CsvVoucher(String id, String voucherType, String benefitValue) { + public CsvVoucher(String id, String voucherType, String benefitValue, String createdAt) { this.id = id; this.voucherType = voucherType; this.benefitValue = benefitValue; + this.createdAt = createdAt; } public static CsvVoucher of(Voucher voucher) { return new CsvVoucher(voucher.getId().toString(), voucher.getVoucherType().getVoucherTypeName(), - voucher.getBenefitValue().toString()); + voucher.getBenefitValue().toString(), + voucher.getCreatedAt().toString() + ); } public Voucher toEntity() { return new Voucher(UUID.fromString(id), VoucherTypeEnum.of(voucherType).getVoucherType(Integer.valueOf(benefitValue)), - Integer.valueOf(benefitValue)); + Integer.valueOf(benefitValue), + SqlConverter.toLocalDateTime(createdAt)); } public String getId() { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java new file mode 100644 index 0000000000..287ea8005d --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java @@ -0,0 +1,42 @@ +package com.programmers.springbootbasic.domain.voucher.presentation.dto; + +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import jakarta.annotation.Nullable; +import java.time.LocalDate; + +public class VoucherCriteria { + + @Nullable + private LocalDate startDate; + @Nullable + private LocalDate endDate; + @Nullable + private VoucherTypeEnum type; + + public VoucherCriteria(LocalDate startDate, LocalDate endDate, VoucherTypeEnum type) { + this.startDate = startDate; + this.endDate = endDate; + this.type = type; + } + + public VoucherCriteria() { + } + + @Nullable + public LocalDate getStartDate() { + return startDate; + } + + @Nullable + public LocalDate getEndDate() { + if (endDate == null) { + return null; + } + return endDate.plusDays(1); + } + + @Nullable + public VoucherTypeEnum getType() { + return type; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java index 645cee853d..a1ba7c876f 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherResponse.java @@ -1,6 +1,7 @@ package com.programmers.springbootbasic.domain.voucher.presentation.dto; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.time.LocalDateTime; import java.util.UUID; public class VoucherResponse { @@ -8,16 +9,20 @@ public class VoucherResponse { private final UUID id; private final String voucherType; private final Integer benefitValue; + private final LocalDateTime createdAt; - public VoucherResponse(UUID id, String voucherType, Integer benefitValue) { + public VoucherResponse(UUID id, String voucherType, Integer benefitValue, + LocalDateTime createdAt + ) { this.id = id; this.voucherType = voucherType; this.benefitValue = benefitValue; + this.createdAt = createdAt; } public static VoucherResponse of(Voucher voucher) { return new VoucherResponse(voucher.getId(), voucher.getVoucherType().getVoucherTypeName(), - voucher.getBenefitValue()); + voucher.getBenefitValue(), voucher.getCreatedAt()); } @Override @@ -41,4 +46,8 @@ public String getVoucherType() { public Integer getBenefitValue() { return benefitValue; } + + public LocalDateTime getCreatedAt() { + return createdAt; + } } From e09ea787665021cdb006dfb8b2e713bc76e962bd Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:03:58 +0900 Subject: [PATCH 74/97] =?UTF-8?q?[add]=20rest=20api=20=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/VoucherRestController.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java new file mode 100644 index 0000000000..333f7ede55 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java @@ -0,0 +1,63 @@ +package com.programmers.springbootbasic.domain.voucher.presentation; + +import com.programmers.springbootbasic.domain.voucher.application.VoucherService; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import java.util.List; +import java.util.UUID; +import org.springframework.context.annotation.Profile; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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; + +@Profile(value = "tomcat") +@RestController +@RequestMapping("/api/voucher") +public class VoucherRestController { + + private final VoucherService voucherService; + + public VoucherRestController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + @PostMapping("/") + public void createVoucher(@RequestBody CreateVoucherRequest request) { + voucherService.create(request); + } + + @GetMapping("/vouchers") + public List getAllVouchers() { + return voucherService.findAll(); + } + + @GetMapping("/vouchers/search") + public List getVouchersByCriteria( + @ModelAttribute VoucherCriteria criteria + ) { + return voucherService.findByCriteria(criteria); + } + + @GetMapping("/{id}") + public VoucherResponse getVoucherById(@PathVariable UUID id) { + return voucherService.findById(id); + } + + @DeleteMapping("/{id}") + public void deleteVoucherById(@PathVariable UUID id) { + voucherService.deleteById(id); + } + + @PutMapping("/{id}") + public void updateVoucher(@PathVariable UUID id, UpdateVoucherRequest request) { + voucherService.update(id, request); + } +} From 0e8fdb11b2083cfe65e5fb66d3347baee7adb5e1 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:05:20 +0900 Subject: [PATCH 75/97] =?UTF-8?q?[change]=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/CreateVoucherRequest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index 556c3ef47e..38215386ae 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -3,26 +3,26 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import java.time.LocalDateTime; import java.util.UUID; public class CreateVoucherRequest { - private final VoucherType voucherType; - private final Integer benefitValue; + private VoucherType voucherType; + private Integer benefitValue; - private CreateVoucherRequest(VoucherType voucherType, Integer benefitValue) { - this.voucherType = voucherType; + private CreateVoucherRequest(VoucherTypeEnum voucherType, Integer benefitValue) { + this.voucherType = voucherType.getVoucherType(benefitValue); this.benefitValue = benefitValue; } - public static CreateVoucherRequest of(String stringVoucherType, Integer benefitValue) { - VoucherTypeEnum voucherEnum = VoucherTypeEnum.of(stringVoucherType); - VoucherType voucherType = voucherEnum.getVoucherType(benefitValue); - return new CreateVoucherRequest(voucherType, benefitValue); + // TODO: String 으로 받을지 Enum 으로 받을지는 고민해보자. + public static CreateVoucherRequest of(VoucherTypeEnum stringVoucherType, Integer benefitValue) { + return new CreateVoucherRequest(stringVoucherType, benefitValue); } - public Voucher toEntity(UUID id) { - return new Voucher(id, voucherType, benefitValue); + public Voucher toEntity(UUID id, LocalDateTime createdAt) { + return new Voucher(id, voucherType, benefitValue, createdAt); } public VoucherType getVoucherType() { From 468e7502a75d473cda224689a5574a4c8f1c39d2 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:06:26 +0900 Subject: [PATCH 76/97] =?UTF-8?q?[add]=20=ED=94=84=EB=A1=9C=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../springbootbasic/VoucherApplication.java | 21 ++++++++++++++++++- src/main/resources/application-command.yml | 6 ++++++ src/main/resources/application-tomcat.yml | 4 ++++ src/main/resources/application.yml | 9 ++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/application-command.yml create mode 100644 src/main/resources/application-tomcat.yml diff --git a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java index 670aa18082..358c4f8299 100644 --- a/src/main/java/com/programmers/springbootbasic/VoucherApplication.java +++ b/src/main/java/com/programmers/springbootbasic/VoucherApplication.java @@ -3,19 +3,38 @@ import com.programmers.springbootbasic.exception.AppExceptionHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; @SpringBootApplication public class VoucherApplication { + private final ConfigurableApplicationContext applicationContext; + + public VoucherApplication(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } public static void main(String[] args) { - ConfigurableApplicationContext applicationContext = SpringApplication.run( + SpringApplication.run( VoucherApplication.class, args ); + } + @Profile(value = "command") + @Bean + public void init() { startAppWithErrorHandler(applicationContext); } + @Profile(value = "tomcat") + @Bean + public ServletWebServerFactory servletTomcatServerContainer() { + return new TomcatServletWebServerFactory(); + } + private static void startAppWithErrorHandler( ConfigurableApplicationContext applicationContext ) { diff --git a/src/main/resources/application-command.yml b/src/main/resources/application-command.yml new file mode 100644 index 0000000000..32ad8ae2b9 --- /dev/null +++ b/src/main/resources/application-command.yml @@ -0,0 +1,6 @@ +spring: + config: + activate: + on-profile: command + main: + web-application-type: none diff --git a/src/main/resources/application-tomcat.yml b/src/main/resources/application-tomcat.yml new file mode 100644 index 0000000000..5399c99624 --- /dev/null +++ b/src/main/resources/application-tomcat.yml @@ -0,0 +1,4 @@ +spring: + config: + activate: + on-profile: tomcat diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b7f485382c..9b2de0baec 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,15 @@ server: shutdown: graceful spring: + profiles: + active: command + group: + tomcat: + - tomcat + command: + - command + test: + - test lifecycle: timeout-per-shutdown-phase: 10s datasource: From ab08bb069c3861bfab202092ddd4f5f335d28f4f Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:06:42 +0900 Subject: [PATCH 77/97] =?UTF-8?q?web=20,=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EA=B4=80=EB=A0=A8=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 8a08f339ac..2addf57386 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,13 @@ dependencies { // mysql implementation 'com.mysql:mysql-connector-j' + + // web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + } tasks.named('test') { From a2be20084132860dcd2f84060900238bb5eac60e Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:34:45 +0900 Subject: [PATCH 78/97] =?UTF-8?q?[change]=20test=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserVoucherWalletServiceTest.java | 10 +-- .../JdbcUserVoucherWalletRepositoryTest.java | 2 +- .../application/VoucherServiceTest.java | 19 +++-- .../JdbcVoucherRepositoryTest.java | 78 ++++++++++++++++--- 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java index 2a249b1e51..3a3556d512 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/application/UserVoucherWalletServiceTest.java @@ -73,7 +73,7 @@ void success_create() { Optional.of(new User(userId, existedNickname, false))); when(voucherRepository.findById(existedUUID)).thenReturn( Optional.of( - new Voucher(existedUUID, new FixedAmountVoucher(100), 100) + new Voucher(existedUUID, new FixedAmountVoucher(100), 100, timeGenerator.now()) ) ); when(userVoucherWalletRepository.save(any())) @@ -132,7 +132,7 @@ void success_findUserByVoucherId() { // given var existedUUID = UUID.randomUUID(); when(voucherRepository.findById(existedUUID)).thenReturn( - Optional.of(new Voucher(existedUUID, new FixedAmountVoucher(100), 100)) + Optional.of(new Voucher(existedUUID, new FixedAmountVoucher(100), 100, timeGenerator.now())) ); when(userVoucherWalletRepository.findUserByVoucherId(existedUUID)).thenReturn( List.of( @@ -154,7 +154,7 @@ void success_findUserByVoucherId_duplicatedUserVoucher() { // given var existedUUID = UUID.randomUUID(); when(voucherRepository.findById(existedUUID)).thenReturn( - Optional.of(new Voucher(existedUUID, new FixedAmountVoucher(100), 100)) + Optional.of(new Voucher(existedUUID, new FixedAmountVoucher(100), 100, timeGenerator.now())) ); when(userVoucherWalletRepository.findUserByVoucherId(existedUUID)).thenReturn( List.of( @@ -196,9 +196,9 @@ void success_findVoucherByUserNickname() { when(userVoucherWalletRepository.findVoucherByUserId(userId)).thenReturn( List.of( new UserVoucherWalletWithVoucher(1L, new Voucher(UUID.randomUUID(), - new FixedAmountVoucher(100), 100)), + new FixedAmountVoucher(100), 100, timeGenerator.now())), new UserVoucherWalletWithVoucher(2L, new Voucher(UUID.randomUUID(), - new FixedAmountVoucher(100), 100)) + new FixedAmountVoucher(100), 100, timeGenerator.now())) ) ); diff --git a/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java index b0cbecb5df..61de865eec 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/userVoucherWallet/infrastructure/JdbcUserVoucherWalletRepositoryTest.java @@ -361,7 +361,7 @@ private User saveUser(String nickname) { private Voucher saveVoucher(UUID id) { return voucherRepository.save( - new Voucher(id, new FixedAmountVoucher(100), 100) + new Voucher(id, new FixedAmountVoucher(100), 100, timeGenerator.now()) ); } diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java index d1425a139b..df9a0951cb 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java @@ -8,6 +8,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import com.programmers.springbootbasic.common.TimeGenerator; +import com.programmers.springbootbasic.domain.TestTimeGenerator; import com.programmers.springbootbasic.domain.TestVoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; @@ -34,6 +36,7 @@ class VoucherServiceTest { @Mock private VoucherRepository voucherRepository; private VoucherIdGenerator idGenerator; + private TimeGenerator timeGenerator = new TestTimeGenerator(); @BeforeEach public void setUp() { @@ -46,10 +49,10 @@ public void setUp() { @Test void success_create() { // given - CreateVoucherRequest request = CreateVoucherRequest.of(VoucherTypeEnum.FIXED.name(), 10); + CreateVoucherRequest request = CreateVoucherRequest.of(VoucherTypeEnum.FIXED, 10); when(voucherRepository.save(any(Voucher.class))).thenReturn( new Voucher(idGenerator.generate(), request.getVoucherType(), - request.getBenefitValue())); + request.getBenefitValue(), timeGenerator.now())); // when var result = voucherService.create(request); @@ -64,8 +67,10 @@ void success_findAll() { // given when(voucherRepository.findAll()).thenReturn( List.of( - new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), 10), - new Voucher(idGenerator.generate(), VoucherTypeEnum.PERCENT.getVoucherType(10), 10) + new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), 10, + timeGenerator.now()), + new Voucher(idGenerator.generate(), VoucherTypeEnum.PERCENT.getVoucherType(10), 10, + timeGenerator.now()) ) ); @@ -84,7 +89,7 @@ void success_findAll() { void success_findById() { // given var voucher = new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), - 10); + 10, timeGenerator.now()); when(voucherRepository.findById(idGenerator.generate())).thenReturn( java.util.Optional.of(voucher)); @@ -112,7 +117,7 @@ void fail_findById() { void success_deleteById() { // given var voucher = new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), - 10); + 10, timeGenerator.now()); when(voucherRepository.findById(voucher.getId())).thenReturn(Optional.of(voucher)); when(voucherRepository.deleteById(voucher.getId())).thenReturn(1); @@ -138,7 +143,7 @@ void fail_deleteById() { void success_update() { // given var voucher = new Voucher(idGenerator.generate(), VoucherTypeEnum.FIXED.getVoucherType(10), - 10); + 10, timeGenerator.now()); UpdateVoucherRequest request = new UpdateVoucherRequest(VoucherTypeEnum.PERCENT, 10); when(voucherRepository.findById(voucher.getId())).thenReturn( java.util.Optional.of(voucher)); diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java index d42a63d030..f632e572dd 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/infrastructure/JdbcVoucherRepositoryTest.java @@ -2,12 +2,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import com.programmers.springbootbasic.common.TimeGenerator; +import com.programmers.springbootbasic.domain.TestTimeGenerator; import com.programmers.springbootbasic.domain.voucher.domain.ProdVoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherIdGenerator; import com.programmers.springbootbasic.domain.voucher.domain.VoucherRepository; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.FixedAmountVoucher; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.PercentDiscountVoucher; +import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,12 +34,14 @@ class JdbcVoucherRepositoryTest { @Autowired private VoucherRepository voucherRepository; private final VoucherIdGenerator idGenerator = new ProdVoucherIdGenerator(); + private final TimeGenerator timeGenerator = new TestTimeGenerator(); @DisplayName("Voucher를 저장할 수 있다.") @Test void success_save() { // given - Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + timeGenerator.now()); // when Voucher savedVoucher = voucherRepository.save(voucher); @@ -45,7 +55,8 @@ void success_save() { @Test void success_findById() { // given - Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + timeGenerator.now()); voucherRepository.save(voucher); // when @@ -59,8 +70,10 @@ void success_findById() { @Test void success_findAll() { // given - Voucher voucher1 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); - Voucher voucher2 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(200), 200); + Voucher voucher1 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + timeGenerator.now()); + Voucher voucher2 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(200), 200, + timeGenerator.now()); voucherRepository.save(voucher1); voucherRepository.save(voucher2); @@ -75,7 +88,8 @@ void success_findAll() { @Test void success_deleteById() { // given - Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + timeGenerator.now()); voucherRepository.save(voucher); // when @@ -103,9 +117,11 @@ void fail_deleteById_notFound() { @Test void success_update() { // given - Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + timeGenerator.now()); voucherRepository.save(voucher); - Voucher updatedVoucher = new Voucher(voucher.getId(), new FixedAmountVoucher(200), 200); + Voucher updatedVoucher = new Voucher(voucher.getId(), new FixedAmountVoucher(200), 200, + timeGenerator.now()); // when int result = voucherRepository.update(updatedVoucher); @@ -120,11 +136,13 @@ void success_update() { @Test void fail_update_missingFields() { // given - Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100); + Voucher voucher = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + timeGenerator.now()); voucherRepository.save(voucher); - Voucher incompleteVoucher = new Voucher(voucher.getId(), null, 200); // Type 정보 누락 + Voucher incompleteVoucher = new Voucher(voucher.getId(), null, 200, + timeGenerator.now()); // Type 정보 누락 Voucher incompleteVoucher2 = new Voucher(voucher.getId(), new FixedAmountVoucher(200), - null); // BenefitValue 정보 누락 + null, timeGenerator.now()); // BenefitValue 정보 누락 // when && then assertThatThrownBy(() -> voucherRepository.update(incompleteVoucher)) @@ -133,4 +151,44 @@ void fail_update_missingFields() { .isInstanceOf(DataAccessException.class); } + + @Test + void findByCriteria() { + // given + Voucher voucher1 = new Voucher(idGenerator.generate(), new FixedAmountVoucher(100), 100, + LocalDateTime.of(2023, 10, 1, 0, 0, 0)); + Voucher voucher2 = new Voucher(idGenerator.generate(), new PercentDiscountVoucher(50), 50, + LocalDateTime.of(2023, 10, 3, 0, 0, 0)); + voucherRepository.save(voucher1); + voucherRepository.save(voucher2); + + // when + var dateResult = voucherRepository.findByCriteria(new VoucherCriteria( + LocalDate.of(2023, 10, 1), + LocalDate.of(2023, 10, 1), + null + )); + + var typeResult = voucherRepository.findByCriteria(new VoucherCriteria( + null, + null, + VoucherTypeEnum.FIXED + )); + + var dateAndTypeResult = voucherRepository.findByCriteria(new VoucherCriteria( + LocalDate.of(2023, 10, 1), + LocalDate.of(2023, 10, 1), + VoucherTypeEnum.FIXED + )); + + // then + assertThat(dateResult).hasSize(1).extracting("createdAt") + .containsExactly(LocalDateTime.of(2023, 10, 1, 0, 0, 0)); + assertThat(typeResult).hasSize(1).extracting("voucherType") + .containsExactly(new FixedAmountVoucher(100)); + assertThat(dateAndTypeResult).hasSize(1).extracting("createdAt", "voucherType") + .containsExactly( + tuple(LocalDateTime.of(2023, 10, 1, 0, 0, 0), + new FixedAmountVoucher(100))); + } } From f9f0cfe11384734161d76bb036f7af0b2bec348c Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:47:00 +0900 Subject: [PATCH 79/97] =?UTF-8?q?[fix]=20delete=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/UserVoucherWalletThymeController.java | 3 ++- .../templates/userVoucher/voucherByUserNicknameResult.html | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java index bac2ed97ce..acbce9e773 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java @@ -9,6 +9,7 @@ import java.util.UUID; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; @@ -46,7 +47,7 @@ public String registerUserVoucher( return "userVoucher/registerationComplete"; } - @PostMapping("/delete/{id}") + @DeleteMapping ("/delete/{id}") public String deleteUserVoucher(@PathVariable Long id) { userVoucherWalletService.deleteById(id); return "userVoucher/deleteComplete"; diff --git a/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html b/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html index 8e8a7e3f51..d8de8160fe 100644 --- a/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html +++ b/src/main/resources/templates/userVoucher/voucherByUserNicknameResult.html @@ -157,8 +157,7 @@

Vouchers of View -
- + From 193f7eaff8e755e22e1c012650bd10b1a6152f1d Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:48:26 +0900 Subject: [PATCH 80/97] =?UTF-8?q?[fix]=20path=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/home.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index ac0bbecb43..257ed85d18 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -139,7 +139,7 @@

전체 바우처 조회

- +

차단 유저 조회

차단 된 유저들의 목록을 조회 할 수 있습니다.

@@ -149,7 +149,7 @@

차단 유저 조회

- +

내 바우처 찾기

사용자의 바우처를 조회 할 수 있습니다.

From 4b7edca52430fb060b86dee1f908f4a370a42706 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 04:51:56 +0900 Subject: [PATCH 81/97] =?UTF-8?q?[fix]=20int=20=ED=95=9C=EA=B3=84=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/voucher/register.html | 2 +- src/main/resources/templates/voucher/update.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/templates/voucher/register.html b/src/main/resources/templates/voucher/register.html index f1afb57826..fc2d473ab1 100644 --- a/src/main/resources/templates/voucher/register.html +++ b/src/main/resources/templates/voucher/register.html @@ -11,7 +11,7 @@ if (type === 'FIXED') { benefitInput.setAttribute('min', '1'); - benefitInput.removeAttribute('max'); + benefitInput.setAttribute('max', '2147483647'); } else if (type === 'PERCENT') { benefitInput.setAttribute('min', '1'); benefitInput.setAttribute('max', '100'); diff --git a/src/main/resources/templates/voucher/update.html b/src/main/resources/templates/voucher/update.html index dd78d427fe..1af9461d53 100644 --- a/src/main/resources/templates/voucher/update.html +++ b/src/main/resources/templates/voucher/update.html @@ -12,7 +12,7 @@ if (type === 'FIXED') { benefitInput.setAttribute('min', '1'); - benefitInput.removeAttribute('max'); + benefitInput.setAttribute('max', '2147483647'); } else if (type === 'PERCENT') { benefitInput.setAttribute('min', '1'); benefitInput.setAttribute('max', '100'); From d6bb9b79e8a221ea3f5d588bee0171b1a2afa063 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:14:55 +0900 Subject: [PATCH 82/97] =?UTF-8?q?[fix]=20test=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/UserServiceTest.java | 5 ++--- .../application/VoucherServiceTest.java | 4 +++- .../dto/CreateVoucherRequestTest.java | 18 ------------------ 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java index af2455dfff..5e46812064 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/user/application/UserServiceTest.java @@ -1,7 +1,6 @@ package com.programmers.springbootbasic.domain.user.application; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.tuple; import static org.mockito.Mockito.when; import com.programmers.springbootbasic.domain.user.domain.UserRepository; @@ -42,7 +41,7 @@ void success_findBlacklistedUsers() { // then assertThat(result).hasSize(1); assertThat(result) - .extracting("nickname", "blocked") - .containsExactlyInAnyOrder(tuple("user1", true)); + .extracting("nickname") + .containsExactlyInAnyOrder("user1"); } } diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java index df9a0951cb..8948b71a7b 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/application/VoucherServiceTest.java @@ -36,13 +36,15 @@ class VoucherServiceTest { @Mock private VoucherRepository voucherRepository; private VoucherIdGenerator idGenerator; - private TimeGenerator timeGenerator = new TestTimeGenerator(); + private TimeGenerator timeGenerator; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); this.idGenerator = new TestVoucherIdGenerator(); ReflectionTestUtils.setField(voucherService, "idGenerator", idGenerator); + this.timeGenerator = new TestTimeGenerator(); + ReflectionTestUtils.setField(voucherService, "timeGenerator", timeGenerator); } @DisplayName("유효한 정보의 바우처를 생성 할 수 있다.") diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java index ebeae89055..e68ca48463 100644 --- a/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequestTest.java @@ -1,11 +1,8 @@ package com.programmers.springbootbasic.domain.voucher.presentation.dto; -import static com.programmers.springbootbasic.exception.ErrorCode.INVALID_VOUCHER; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; -import com.programmers.springbootbasic.exception.exceptionClass.MenuException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,19 +22,4 @@ void success_of() { assertThat(request.getVoucherType()).isNotNull(); assertThat(request.getBenefitValue()).isEqualTo(benefitValue); } - - @DisplayName("올바르지 않은 VoucherType 문자열로 of 메서드를 호출할 경우 예외를 발생시킨다.") - @Test - void fail_of() { - // given - String invalidStringVoucherType = "INVALID_TYPE"; - Integer benefitValue = 100; - - // then - assertThatThrownBy( - () -> CreateVoucherRequest.of(VoucherTypeEnum.of(invalidStringVoucherType), - benefitValue)) - .isInstanceOf(MenuException.class) - .hasMessageContaining(INVALID_VOUCHER.getMessage()); - } } From 504f03ed8a23120667b05d4ed4b365c0d5c227bc Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:16:06 +0900 Subject: [PATCH 83/97] =?UTF-8?q?[add]=20=ED=95=84=ED=84=B0=EB=A7=81=20dto?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../presentation/VoucherRestController.java | 3 +- .../presentation/dto/VoucherCriteria.java | 10 +++-- .../springbootbasic/exception/ErrorCode.java | 1 + .../exception/ErrorResponse.java | 29 +++++++++++++ .../exception/GlobalRestException.java | 41 +++++++++++++++++-- 6 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/programmers/springbootbasic/exception/ErrorResponse.java diff --git a/build.gradle b/build.gradle index 2addf57386..d4332a0bc2 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,8 @@ dependencies { // thymeleaf implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + // validation + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java index 333f7ede55..edce79c9d0 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java @@ -5,6 +5,7 @@ import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import jakarta.validation.Valid; import java.util.List; import java.util.UUID; import org.springframework.context.annotation.Profile; @@ -41,7 +42,7 @@ public List getAllVouchers() { @GetMapping("/vouchers/search") public List getVouchersByCriteria( - @ModelAttribute VoucherCriteria criteria + @Valid @ModelAttribute VoucherCriteria criteria ) { return voucherService.findByCriteria(criteria); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java index 287ea8005d..c6af74b3b1 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/VoucherCriteria.java @@ -2,13 +2,18 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import jakarta.annotation.Nullable; +import jakarta.validation.constraints.PastOrPresent; import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; public class VoucherCriteria { - + @PastOrPresent @Nullable + @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate startDate; + @PastOrPresent @Nullable + @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate endDate; @Nullable private VoucherTypeEnum type; @@ -19,9 +24,6 @@ public VoucherCriteria(LocalDate startDate, LocalDate endDate, VoucherTypeEnum t this.type = type; } - public VoucherCriteria() { - } - @Nullable public LocalDate getStartDate() { return startDate; diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java index 43feaf2686..0c6ea854a4 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorCode.java @@ -10,6 +10,7 @@ public enum ErrorCode { INVALID_FIXED_VOUCHER_BENEFIT("고정 할인 금액은 0원 이상이어야 합니다.", HttpStatus.BAD_REQUEST), INVALID_PERCENT_VOUCHER_BENEFIT("비율 할인 금액은 0% 이상 100% 이하여야 합니다.", HttpStatus.BAD_REQUEST), INVALID_FILE_PATH("올바르지 않은 파일 경로입니다.", HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_INPUT("올바르지 않은 입력입니다.", HttpStatus.BAD_REQUEST), EXIT("시스템을 종료합니다.", HttpStatus.INTERNAL_SERVER_ERROR), NOT_FOUND_VOUCHER("바우처를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), NOT_FOUND_USER("사용자를 찾을 수 없습니다.", HttpStatus.BAD_REQUEST), diff --git a/src/main/java/com/programmers/springbootbasic/exception/ErrorResponse.java b/src/main/java/com/programmers/springbootbasic/exception/ErrorResponse.java new file mode 100644 index 0000000000..4f5f40e38a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/ErrorResponse.java @@ -0,0 +1,29 @@ +package com.programmers.springbootbasic.exception; + +import lombok.Builder; + +public class ErrorResponse { + + private final String message; + private final int status; + private final String code; + + @Builder + private ErrorResponse(String message, int status, String code) { + this.message = message; + this.status = status; + this.code = code; + } + + public String getMessage() { + return message; + } + + public int getStatus() { + return status; + } + + public String getCode() { + return code; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java index 92732b1c00..c9ba8bf222 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java @@ -1,18 +1,53 @@ package com.programmers.springbootbasic.exception; +import com.programmers.springbootbasic.domain.voucher.presentation.VoucherRestController; import com.programmers.springbootbasic.exception.exceptionClass.CustomException; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +@ControllerAdvice(assignableTypes = VoucherRestController.class) public class GlobalRestException { @ExceptionHandler({ CustomException.class, }) - public ResponseEntity handleUserException(CustomException ex) { + public ResponseEntity handleUserException(CustomException ex) { return ResponseEntity .status(ex.getErrorCode().getHttpStatus()) - .body(ex.getMessage()); + .body(ErrorResponse.builder() + .message(ex.getMessage()) + .status(ex.getErrorCode().getHttpStatus().value()) + .code(ex.getErrorCode().name()) + .build()); + } + + @ExceptionHandler(value = { + BindException.class, + MethodArgumentNotValidException.class + }) + public ResponseEntity validationError(BindException e) { + BindingResult bindingResult = e.getBindingResult(); + + StringBuilder builder = new StringBuilder(); + for (FieldError fieldError : bindingResult.getFieldErrors()) { + builder.append("["); + builder.append(fieldError.getField()); + builder.append("](은)는 "); + builder.append(fieldError.getDefaultMessage()); + builder.append(" 입력된 값: ["); + builder.append(fieldError.getRejectedValue()); + builder.append("]\n"); + } + return ResponseEntity.badRequest().body(ErrorResponse.builder() + .message(builder.toString()) + .status(400) + .code(ErrorCode.INVALID_INPUT.name()) + .build()); } @ExceptionHandler({ @@ -20,7 +55,7 @@ public ResponseEntity handleUserException(CustomException ex) { }) public ResponseEntity handleException(Exception ex) { return ResponseEntity - .status(500) + .internalServerError() .body(ex.getMessage()); } From 39203372a0984f308c3d2955f274674a5d7b8db4 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:16:50 +0900 Subject: [PATCH 84/97] =?UTF-8?q?[add]=20=EB=A9=94=EC=9D=B8=20=ED=99=88=20?= =?UTF-8?q?=EB=8F=99=EC=98=81=EC=83=81=20=EC=97=90=EB=9F=AC=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/mycode.avif | Bin 0 -> 158958 bytes src/main/resources/templates/home.html | 85 +++++++++--------- .../userVoucher/userByVoucherIdResult.html | 2 +- 3 files changed, 42 insertions(+), 45 deletions(-) create mode 100644 src/main/resources/static/mycode.avif diff --git a/src/main/resources/static/mycode.avif b/src/main/resources/static/mycode.avif new file mode 100644 index 0000000000000000000000000000000000000000..8ed2b1bf473408d39406359e3c7bdf8a7a77438a GIT binary patch literal 158958 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%0ANJ00A*9A36h*wbi;4X_6SngF`w_&)fcx0+b0g& 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

@mSYA>-7S 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 b$(Mvrxw3RRzw$27a~@aVMw?Rir-=(BeAMh{ literal 0 HcmV?d00001 diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 257ed85d18..d70d5fd44c 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -4,6 +4,33 @@ Home +

-
- - - -
-
- -
- -
- - - - - - + diff --git a/src/main/resources/templates/userVoucher/userByVoucherIdResult.html b/src/main/resources/templates/userVoucher/userByVoucherIdResult.html index 4a39aa79ac..b6ff26ec32 100644 --- a/src/main/resources/templates/userVoucher/userByVoucherIdResult.html +++ b/src/main/resources/templates/userVoucher/userByVoucherIdResult.html @@ -134,7 +134,7 @@

User List

-

No users are currently has Voucher.

+

No users.

From cc6d66a0cd0de5608008212bbb570d808c23e7d4 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:28:34 +0900 Subject: [PATCH 85/97] =?UTF-8?q?[change]=20=EC=97=90=EB=9F=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{thyme => exception}/CustomErrorController.java | 2 +- ...GlobalRestException.java => GlobalRestExceptionHandler.java} | 2 +- ...alExceptionHandler.java => GlobalThymeExceptionHandler.java} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/com/programmers/springbootbasic/{thyme => exception}/CustomErrorController.java (87%) rename src/main/java/com/programmers/springbootbasic/exception/{GlobalRestException.java => GlobalRestExceptionHandler.java} (98%) rename src/main/java/com/programmers/springbootbasic/exception/{GlobalExceptionHandler.java => GlobalThymeExceptionHandler.java} (93%) diff --git a/src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java b/src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java similarity index 87% rename from src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java rename to src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java index 65603348da..e1d1978a64 100644 --- a/src/main/java/com/programmers/springbootbasic/thyme/CustomErrorController.java +++ b/src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java @@ -1,4 +1,4 @@ -package com.programmers.springbootbasic.thyme; +package com.programmers.springbootbasic.exception; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.stereotype.Controller; diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalRestExceptionHandler.java similarity index 98% rename from src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java rename to src/main/java/com/programmers/springbootbasic/exception/GlobalRestExceptionHandler.java index c9ba8bf222..7c73f84ad8 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/GlobalRestException.java +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalRestExceptionHandler.java @@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice(assignableTypes = VoucherRestController.class) -public class GlobalRestException { +public class GlobalRestExceptionHandler { @ExceptionHandler({ CustomException.class, diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java similarity index 93% rename from src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java rename to src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java index c051ced7d7..5d9e52c4ab 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java @@ -6,7 +6,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice -public class GlobalExceptionHandler { +public class GlobalThymeExceptionHandler { @ExceptionHandler({ CustomException.class, From 50d0ebb8a9be230a4b63eda817570e3c528d67da Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:46:30 +0900 Subject: [PATCH 86/97] =?UTF-8?q?[change]=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomErrorController.java | 14 -------------- .../exception/GlobalThymeExceptionHandler.java | 7 +++++++ 2 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java diff --git a/src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java b/src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java deleted file mode 100644 index e1d1978a64..0000000000 --- a/src/main/java/com/programmers/springbootbasic/exception/CustomErrorController.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.programmers.springbootbasic.exception; - -import org.springframework.boot.web.servlet.error.ErrorController; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -public class CustomErrorController implements ErrorController { - - @RequestMapping("/error") - public String handleError() { - return "defaultError"; - } -} diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java index 5d9e52c4ab..45ef6599e6 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java @@ -16,4 +16,11 @@ public String handleUserException(CustomException ex, Model model) { model.addAttribute("message", ex.getMessage()); return "customError"; } + + @ExceptionHandler({ + Exception.class, + }) + public String handleException() { + return "defaultError"; + } } From 392e09923af765f7e10a17a206132668c0c6ce6a Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:34:17 +0900 Subject: [PATCH 87/97] =?UTF-8?q?[fix]=20@RequestBody=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/presentation/VoucherRestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java index edce79c9d0..86f763ee87 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java @@ -58,7 +58,7 @@ public void deleteVoucherById(@PathVariable UUID id) { } @PutMapping("/{id}") - public void updateVoucher(@PathVariable UUID id, UpdateVoucherRequest request) { + public void updateVoucher(@PathVariable UUID id, @RequestBody UpdateVoucherRequest request) { voucherService.update(id, request); } } From 9908e6344ef4f28acf81e5f4b897ca39e42c1a94 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 18:43:49 +0900 Subject: [PATCH 88/97] =?UTF-8?q?[fix]=20path=20=EC=8B=A4=EC=88=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/presentation/VoucherRestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java index 86f763ee87..dde52e25ad 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java @@ -30,7 +30,7 @@ public VoucherRestController(VoucherService voucherService) { this.voucherService = voucherService; } - @PostMapping("/") + @PostMapping public void createVoucher(@RequestBody CreateVoucherRequest request) { voucherService.create(request); } From bc2c47f2fa49cffa1981ef2d3a9f0f1b70c50fc4 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:33:15 +0900 Subject: [PATCH 89/97] =?UTF-8?q?[fix]=20=EB=B9=88=20=EA=B0=92=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/templates/user/register.html | 15 +++++++++++++-- .../templates/userVoucher/enterUserNickname.html | 4 +++- .../templates/userVoucher/enterVoucherId.html | 7 +++++-- .../resources/templates/userVoucher/register.html | 7 ++++++- .../resources/templates/voucher/register.html | 7 +------ 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/resources/templates/user/register.html b/src/main/resources/templates/user/register.html index 368e2be487..1ad9a359d6 100644 --- a/src/main/resources/templates/user/register.html +++ b/src/main/resources/templates/user/register.html @@ -4,6 +4,17 @@ Register +

Sign up


- +
@@ -123,7 +134,7 @@

- +

diff --git a/src/main/resources/templates/userVoucher/enterUserNickname.html b/src/main/resources/templates/userVoucher/enterUserNickname.html index b77f7def10..583db4df13 100644 --- a/src/main/resources/templates/userVoucher/enterUserNickname.html +++ b/src/main/resources/templates/userVoucher/enterUserNickname.html @@ -6,8 +6,10 @@ diff --git a/src/main/resources/templates/userVoucher/enterVoucherId.html b/src/main/resources/templates/userVoucher/enterVoucherId.html index 6aed5d7744..e1b3b3dabf 100644 --- a/src/main/resources/templates/userVoucher/enterVoucherId.html +++ b/src/main/resources/templates/userVoucher/enterVoucherId.html @@ -8,8 +8,11 @@ From 677fb06ca71f0d57caf0fbf682f449c33f60f616 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 19:34:14 +0900 Subject: [PATCH 90/97] =?UTF-8?q?[fix]=20spring=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=AA=BB=EC=9E=A1=EB=8A=94=20error=20=EB=A7=A4=ED=95=91?= =?UTF-8?q?=ED=95=98=EB=8A=94=20ErrorController=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalThymeExceptionHandler.java | 1 + .../exception/NoMappingThymeExceptionHandler.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/exception/NoMappingThymeExceptionHandler.java diff --git a/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java index 45ef6599e6..cac011428d 100644 --- a/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java +++ b/src/main/java/com/programmers/springbootbasic/exception/GlobalThymeExceptionHandler.java @@ -12,6 +12,7 @@ public class GlobalThymeExceptionHandler { CustomException.class, }) public String handleUserException(CustomException ex, Model model) { + System.out.println("GlobalThymeExceptionHandler.handleUserException"); model.addAttribute("error", ex.getErrorCode().getHttpStatus()); model.addAttribute("message", ex.getMessage()); return "customError"; diff --git a/src/main/java/com/programmers/springbootbasic/exception/NoMappingThymeExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/exception/NoMappingThymeExceptionHandler.java new file mode 100644 index 0000000000..e452927457 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/exception/NoMappingThymeExceptionHandler.java @@ -0,0 +1,13 @@ +package com.programmers.springbootbasic.exception; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class NoMappingThymeExceptionHandler implements ErrorController { + @RequestMapping("/error") + public String handleError() { + return "defaultError"; + } +} From 678d99b68bfffbadede70df242848aa68c03cd31 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:03:39 +0900 Subject: [PATCH 91/97] =?UTF-8?q?[change]=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/presentation/dto/CreateVoucherRequest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java index 38215386ae..140ee824cd 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/CreateVoucherRequest.java @@ -3,12 +3,15 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; import com.programmers.springbootbasic.domain.voucher.domain.entity.Voucher; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.UUID; public class CreateVoucherRequest { + @NotNull private VoucherType voucherType; + @NotNull private Integer benefitValue; private CreateVoucherRequest(VoucherTypeEnum voucherType, Integer benefitValue) { From 4a43d7ed83cfe2797d24ab5cd326c413be7abd8e Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:04:03 +0900 Subject: [PATCH 92/97] =?UTF-8?q?[change]=20Nullable=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/voucher/presentation/dto/UpdateVoucherRequest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java index 219c33db1e..e144ba0694 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/dto/UpdateVoucherRequest.java @@ -2,10 +2,12 @@ import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherType; import com.programmers.springbootbasic.domain.voucher.domain.VoucherType.VoucherTypeEnum; +import jakarta.annotation.Nullable; public class UpdateVoucherRequest { - + @Nullable private final VoucherType voucherType; + @Nullable private final Integer benefitValue; public UpdateVoucherRequest(VoucherTypeEnum voucherType, Integer benefitValue) { From 1b60700827970723fbe8c78c488de8123005fdd4 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:04:28 +0900 Subject: [PATCH 93/97] =?UTF-8?q?[change]=20List=20=ED=98=95=EC=8B=9D=20Ob?= =?UTF-8?q?ject=EB=A1=9C=20=EA=B0=90=EC=8B=B8=EC=84=9C=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/VoucherRestController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java index dde52e25ad..36f29f8b3a 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherRestController.java @@ -1,12 +1,12 @@ package com.programmers.springbootbasic.domain.voucher.presentation; +import com.programmers.springbootbasic.common.dto.BaseListResponse; import com.programmers.springbootbasic.domain.voucher.application.VoucherService; import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherCriteria; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; import jakarta.validation.Valid; -import java.util.List; import java.util.UUID; import org.springframework.context.annotation.Profile; import org.springframework.web.bind.annotation.DeleteMapping; @@ -31,20 +31,20 @@ public VoucherRestController(VoucherService voucherService) { } @PostMapping - public void createVoucher(@RequestBody CreateVoucherRequest request) { - voucherService.create(request); + public UUID createVoucher(@Valid @RequestBody CreateVoucherRequest request) { + return voucherService.create(request); } @GetMapping("/vouchers") - public List getAllVouchers() { - return voucherService.findAll(); + public BaseListResponse getAllVouchers() { + return BaseListResponse.from(voucherService.findAll()); } @GetMapping("/vouchers/search") - public List getVouchersByCriteria( + public BaseListResponse getVouchersByCriteria( @Valid @ModelAttribute VoucherCriteria criteria ) { - return voucherService.findByCriteria(criteria); + return BaseListResponse.from(voucherService.findByCriteria(criteria)); } @GetMapping("/{id}") From a2b18b5d15274b61ba7de6714dc012b2c05a2d55 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sat, 4 Nov 2023 00:56:59 +0900 Subject: [PATCH 94/97] =?UTF-8?q?[fix]=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{registerationComplete.html => registrationComplete.html} | 0 .../{registerationComplete.html => registrationComplete.html} | 0 .../{registerationComplete.html => registrationComplete.html} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/templates/user/{registerationComplete.html => registrationComplete.html} (100%) rename src/main/resources/templates/userVoucher/{registerationComplete.html => registrationComplete.html} (100%) rename src/main/resources/templates/voucher/{registerationComplete.html => registrationComplete.html} (100%) diff --git a/src/main/resources/templates/user/registerationComplete.html b/src/main/resources/templates/user/registrationComplete.html similarity index 100% rename from src/main/resources/templates/user/registerationComplete.html rename to src/main/resources/templates/user/registrationComplete.html diff --git a/src/main/resources/templates/userVoucher/registerationComplete.html b/src/main/resources/templates/userVoucher/registrationComplete.html similarity index 100% rename from src/main/resources/templates/userVoucher/registerationComplete.html rename to src/main/resources/templates/userVoucher/registrationComplete.html diff --git a/src/main/resources/templates/voucher/registerationComplete.html b/src/main/resources/templates/voucher/registrationComplete.html similarity index 100% rename from src/main/resources/templates/voucher/registerationComplete.html rename to src/main/resources/templates/voucher/registrationComplete.html From dc0c78b8f8c3f9d7155c140ea2051594f4100103 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sat, 4 Nov 2023 00:57:36 +0900 Subject: [PATCH 95/97] =?UTF-8?q?[change]=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/presentation/UserThymeController.java | 5 +++-- .../presentation/UserVoucherWalletThymeController.java | 5 +++-- .../domain/voucher/presentation/VoucherThymeController.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java index 05c5b7539f..0353ae30ae 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserThymeController.java @@ -2,6 +2,7 @@ import com.programmers.springbootbasic.domain.user.application.UserService; import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; +import jakarta.validation.Valid; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -25,13 +26,13 @@ public String getRegisterPage() { } @PostMapping("/register") - public String createUser(@ModelAttribute CreateUserRequest request, Model model) { + public String createUser(@Valid @ModelAttribute CreateUserRequest request, Model model) { Long id = userService.create(request); model.addAttribute("userNickname", request.getNickname()); model.addAttribute("userId", id); - return "user/registerationComplete"; + return "user/registrationComplete"; } @GetMapping diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java index acbce9e773..b114fa745f 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletThymeController.java @@ -5,6 +5,7 @@ import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.UserOwnedVoucherResponse; import com.programmers.springbootbasic.domain.voucher.application.VoucherService; +import jakarta.validation.Valid; import java.util.List; import java.util.UUID; import org.springframework.stereotype.Controller; @@ -40,11 +41,11 @@ public String getRegisterPage(Model model) { @PostMapping("/register") public String registerUserVoucher( - @ModelAttribute CreateUserVoucherWalletRequest request, Model model + @Valid @ModelAttribute CreateUserVoucherWalletRequest request, Model model ) { Long id = userVoucherWalletService.create(request); model.addAttribute("id", id); - return "userVoucher/registerationComplete"; + return "userVoucher/registrationComplete"; } @DeleteMapping ("/delete/{id}") diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java index 65e4b00a27..ae15f2bf8a 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherThymeController.java @@ -4,6 +4,7 @@ import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import jakarta.validation.Valid; import java.util.List; import java.util.UUID; import org.springframework.context.annotation.Profile; @@ -34,12 +35,12 @@ public String getVoucherPage() { } @PostMapping("/register") - public String createVoucher(@ModelAttribute CreateVoucherRequest request, Model model) { + public String createVoucher(@Valid @ModelAttribute CreateVoucherRequest request, Model model) { voucherService.create(request); model.addAttribute("voucherType", request.getVoucherType().getVoucherTypeName()); model.addAttribute("benefit", request.getBenefitValue()); - return "voucher/registerationComplete"; + return "voucher/registrationComplete"; } @GetMapping("/vouchers") From 88581648d9206e904efa2d95b3ac3a74737721e1 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 5 Nov 2023 03:10:13 +0900 Subject: [PATCH 96/97] =?UTF-8?q?[change]=20=EC=BD=98=EC=86=94=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/presentation/UserController.java | 5 ++++- .../presentation/dto/CreateUserRequest.java | 7 +++++-- .../UserVoucherWalletController.java | 21 ++++++++++++++----- .../dto/CreateUserVoucherWalletRequest.java | 5 +++++ .../presentation/VoucherController.java | 7 +++++-- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java index 117cde4a14..45de6f7c03 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/UserController.java @@ -3,10 +3,13 @@ import com.programmers.springbootbasic.domain.user.application.UserService; import com.programmers.springbootbasic.domain.user.presentation.dto.CreateUserRequest; import com.programmers.springbootbasic.domain.user.presentation.dto.UserResponse; +import jakarta.validation.Valid; import java.util.List; import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; @Controller +@Validated public class UserController { private final UserService userService; @@ -15,7 +18,7 @@ public UserController(UserService userService) { this.userService = userService; } - public void createUser(CreateUserRequest request) { + public void createUser(@Valid CreateUserRequest request) { userService.create(request); } diff --git a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java index 49ce16fa04..801db820e1 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/user/presentation/dto/CreateUserRequest.java @@ -1,11 +1,14 @@ package com.programmers.springbootbasic.domain.user.presentation.dto; import com.programmers.springbootbasic.domain.user.domain.entity.User; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; public class CreateUserRequest { - - private String nickname; + @NotNull + @Length(min = 1, max = 10) + private final String nickname; private CreateUserRequest(String nickname) { this.nickname = nickname; diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java index 6f34d47d80..01dab2f4f1 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/UserVoucherWalletController.java @@ -4,32 +4,43 @@ import com.programmers.springbootbasic.domain.userVoucherWallet.application.UserVoucherWalletService; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.CreateUserVoucherWalletRequest; import com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto.UserOwnedVoucherResponse; +import com.programmers.springbootbasic.util.ConsoleValidator; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import java.util.List; import java.util.UUID; import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; @Controller +@Validated public class UserVoucherWalletController { private final UserVoucherWalletService userVoucherWalletService; + private final ConsoleValidator consoleValidator; - public UserVoucherWalletController(UserVoucherWalletService userVoucherWalletService) { + public UserVoucherWalletController(UserVoucherWalletService userVoucherWalletService, + ConsoleValidator consoleValidator + ) { this.userVoucherWalletService = userVoucherWalletService; + this.consoleValidator = consoleValidator; } - public void createUserVoucher(CreateUserVoucherWalletRequest request) { + public void createUserVoucher(@Valid CreateUserVoucherWalletRequest request) { + consoleValidator.validate(request); userVoucherWalletService.create(request); } - public void deleteUserVoucher(Long id) { + public void deleteUserVoucher(@NotNull Long id) { userVoucherWalletService.deleteById(id); } - public List findUserByVoucherId(UUID voucherId) { + public List findUserByVoucherId(@NotNull UUID voucherId) { return userVoucherWalletService.findUserByVoucherId(voucherId); } - public List findVoucherByUserNickname(String nickname) { + public List findVoucherByUserNickname(@NotEmpty String nickname) { return userVoucherWalletService.findVoucherByUserNickname(nickname); } } diff --git a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java index 610fd4eafc..4d5053e14b 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java +++ b/src/main/java/com/programmers/springbootbasic/domain/userVoucherWallet/presentation/dto/CreateUserVoucherWalletRequest.java @@ -1,12 +1,17 @@ package com.programmers.springbootbasic.domain.userVoucherWallet.presentation.dto; import com.programmers.springbootbasic.domain.userVoucherWallet.domain.entity.UserVoucherWallet; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.UUID; +import org.hibernate.validator.constraints.Length; public class CreateUserVoucherWalletRequest { + @NotNull + @Length(min = 1, max = 10) private final String nickname; + @NotNull private final UUID voucherId; public CreateUserVoucherWalletRequest(String nickname, UUID voucherId) { diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java index 6b655d823c..5ab96e4b00 100644 --- a/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/presentation/VoucherController.java @@ -4,11 +4,14 @@ import com.programmers.springbootbasic.domain.voucher.presentation.dto.CreateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.UpdateVoucherRequest; import com.programmers.springbootbasic.domain.voucher.presentation.dto.VoucherResponse; +import jakarta.validation.Valid; import java.util.List; import java.util.UUID; import org.springframework.stereotype.Controller; +import org.springframework.validation.annotation.Validated; @Controller +@Validated public class VoucherController { private final VoucherService voucherService; @@ -17,7 +20,7 @@ public VoucherController(VoucherService voucherService) { this.voucherService = voucherService; } - public void createVoucher(CreateVoucherRequest request) { + public void createVoucher(@Valid CreateVoucherRequest request) { voucherService.create(request); } @@ -33,7 +36,7 @@ public void deleteVoucherById(UUID id) { voucherService.deleteById(id); } - public void updateVoucher(UUID id, UpdateVoucherRequest request) { + public void updateVoucher(UUID id, @Valid UpdateVoucherRequest request) { voucherService.update(id, request); } From e519620f3133411d085622a2061aa5ebe3ccfa02 Mon Sep 17 00:00:00 2001 From: happyjamy <78072370+happyjamy@users.noreply.github.com> Date: Sun, 5 Nov 2023 03:10:48 +0900 Subject: [PATCH 97/97] =?UTF-8?q?[change]=20Rest=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20return=20object=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dto/BaseListCountResponse.java | 13 +++++++++++++ .../common/dto/BaseListResponse.java | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/com/programmers/springbootbasic/common/dto/BaseListCountResponse.java create mode 100644 src/main/java/com/programmers/springbootbasic/common/dto/BaseListResponse.java diff --git a/src/main/java/com/programmers/springbootbasic/common/dto/BaseListCountResponse.java b/src/main/java/com/programmers/springbootbasic/common/dto/BaseListCountResponse.java new file mode 100644 index 0000000000..a6198f5657 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/dto/BaseListCountResponse.java @@ -0,0 +1,13 @@ +package com.programmers.springbootbasic.common.dto; + +import java.util.List; + +public class BaseListCountResponse extends BaseListResponse { + + private final int count; + + public BaseListCountResponse(List data, int count) { + super(data); + this.count = count; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/dto/BaseListResponse.java b/src/main/java/com/programmers/springbootbasic/common/dto/BaseListResponse.java new file mode 100644 index 0000000000..8c6048837a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/dto/BaseListResponse.java @@ -0,0 +1,16 @@ +package com.programmers.springbootbasic.common.dto; + +import java.util.List; + +public class BaseListResponse { + + private final List data; + + public BaseListResponse(List data) { + this.data = data; + } + + public static BaseListResponse from(List data) { + return new BaseListResponse<>(data); + } +}