From feaa934ebd1ed79eb552bca268f50531bb8aeb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:23:43 +0900 Subject: [PATCH 01/38] feat: project migration spring boot --- .../1-sprint-mission/.gitattributes | 3 + .../1-sprint-mission/.gitignore | 32 ++---- .../1-sprint-mission/README.md | 96 +----------------- .../1-sprint-mission/build.gradle | 40 ++++---- .../gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 +- .../1-sprint-mission/gradlew | 44 +++++--- .../1-sprint-mission/gradlew.bat | 37 ++++--- .../1-sprint-mission/settings.gradle | 3 +- .../discodeit/DiscodeitApplication.java | 13 +++ .../src/main/resources/application.properties | 1 + .../discodeit/DiscodeitApplicationTests.java | 13 +++ 12 files changed, 123 insertions(+), 164 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/.gitattributes create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/.gitattributes b/codeit-bootcamp-spring/1-sprint-mission/.gitattributes new file mode 100644 index 000000000..8af972cde --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/codeit-bootcamp-spring/1-sprint-mission/.gitignore b/codeit-bootcamp-spring/1-sprint-mission/.gitignore index babb7212a..c2065bc26 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/.gitignore +++ b/codeit-bootcamp-spring/1-sprint-mission/.gitignore @@ -1,22 +1,11 @@ +HELP.md .gradle build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Eclipse ### +### STS ### .apt_generated .classpath .factorypath @@ -28,6 +17,15 @@ bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + ### NetBeans ### /nbproject/private/ /nbbuild/ @@ -37,11 +35,3 @@ bin/ ### VS Code ### .vscode/ - -### Mac OS ### -.DS_Store - - -/build -/build/test-results/ -/build/reports/ diff --git a/codeit-bootcamp-spring/1-sprint-mission/README.md b/codeit-bootcamp-spring/1-sprint-mission/README.md index afa5c7d95..b27dfc5e9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/README.md +++ b/codeit-bootcamp-spring/1-sprint-mission/README.md @@ -1,93 +1,5 @@ -간단하게 얼른 하고 끝내버리자. -1. 서비스 구현체를 구현하라. -> 기존 유저 서비스의 구현 +프로젝트 마일스톤 -## 로직 생각해보기 -객체 직렬화를 사용해서 할 경우, 바이너리로 저장된다. 수정을 하고 싶은 경우 특정 부분만 삭제하거나, 수정하는 것이 어렵다 -=> 모든 저장 내용을 읽어들이고 파일을 새롭게 쓰는 방법이 쉬운 방법이다. - -성능적으로 보면, 매우 안좋다고 볼 수 있다. 만약 파일에 저장된 데이터가 매우 많을 경우를 생각하면 읽고 새롭게 쓰고 하면 성능저하가 당연하다. - -데이터가 많을 경우 데이터베이스를 이용하는 것이 좋다. - -요구사항 -기본 요구사항 -File IO를 통한 데이터 영속화 -- [ ] 다음의 조건을 만족하는 서비스 인터페이스의 구현체를 작성하세요. - -- [ ] 클래스 패키지명: com.sprint.mission.discodeit.service.file - -- [ ] 클래스 네이밍 규칙: File[인터페이스 이름] - -- [ ] JCF 대신 FileIO와 객체 직렬화를 활용해 메소드를 구현하세요. - -객체 직렬화/역직렬화 가이드 - -- [ ] Application에서 서비스 구현체를 File*Service로 바꾸어 테스트해보세요. - -서비스 구현체 분석 -- [ ] JCF*Service 구현체와 File*Service 구현체를 비교하여 공통점과 차이점을 발견해보세요. -- [ ] "비즈니스 로직"과 관련된 코드를 식별해보세요. -- [ ] "저장 로직"과 관련된 코드를 식별해보세요. -레포지토리 설계 및 구현 -- [ ] "저장 로직"과 관련된 기능을 도메인 모델 별 인터페이스로 선언하세요. -- [ ] 인터페이스 패키지명: com.sprint.mission.discodeit.repository -- [ ] 인터페이스 네이밍 규칙: [도메인 모델 이름]Repository -- [ ] 다음의 조건을 만족하는 레포지토리 인터페이스의 구현체를 작성하세요. -- [ ] 클래스 패키지명: com.sprint.mission.discodeit.repository.jcf -- [ ] 클래스 네이밍 규칙: JCF[인터페이스 이름] -- [ ] 기존에 구현한 JCF*Service 구현체의 "저장 로직"과 관련된 코드를 참고하여 구현하세요. -- [ ] 다음의 조건을 만족하는 레포지토리 인터페이스의 구현체를 작성하세요. -- [ ] 클래스 패키지명: com.sprint.mission.discodeit.repository.file -- [ ] 클래스 네이밍 규칙: File[인터페이스 이름] -- [ ] 기존에 구현한 File*Service 구현체의 "저장 로직"과 관련된 코드를 참고하여 구현하세요. -심화 요구 사항 -관심사 분리를 통한 레이어 간 의존성 주입 -- [ ] 다음의 조건을 만족하는 서비스 인터페이스의 구현체를 작성하세요. -- [ ] 클래스 패키지명: com.sprint.mission.discodeit.service.basic -- [ ] 클래스 네이밍 규칙: Basic[인터페이스 이름] -- [ ] 기존에 구현한 서비스 구현체의 "비즈니스 로직"과 관련된 코드를 참고하여 구현하세요. -- [ ] 필요한 Repository 인터페이스를 필드로 선언하고 생성자를 통해 초기화하세요. -- [ ] "저장 로직"은 Repository 인터페이스 필드를 활용하세요. (직접 구현하지 마세요.) -- [ ] Basic*Service 구현체를 활용하여 테스트해보세요. -코드 템플릿 - -```java - - -public class JavaApplication { - static User setupUser(UserService userService) { - User user = userService.create("woody", "woody@codeit.com", "woody1234"); - return user; - } - - static Channel setupChannel(ChannelService channelService) { - Channel channel = channelService.create(ChannelType.PUBLIC, "공지", "공지 채널입니다."); - return channel; - } - - static void messageCreateTest(MessageService messageService, Channel channel, User author) { - Message message = messageService.create("안녕하세요.", channel.getId(), author.getId()); - System.out.println("메시지 생성: " + message.getId()); - } - - public static void main(String[] args) { - // 서비스 초기화 - // TODO Basic*Service 구현체를 초기화하세요. - UserService userService; - ChannelService channelService; - MessageService messageService; - - // 셋업 - User user = setupUser(userService); - Channel channel = setupChannel(channelService); - // 테스트 - messageCreateTest(messageService, channel, user); - } -} -``` - -- [ ] JCF*Repository 구현체를 활용하여 테스트해보세요. - -- [ ] File*Repository 구현체를 활용하여 테스트해보세요. - -- [ ] 이전에 작성했던 코드(JCF*Service 또는 File*Service)와 비교해 어떤 차이가 있는지 정리해보세요. \ No newline at end of file +- Java 프로젝트를 Spring 프로젝트로 마이그레이션 +- 의존성 관리를 IoC Container에 위임하도록 리팩토링 +- 비즈니스 로직 고도화 \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/build.gradle b/codeit-bootcamp-spring/1-sprint-mission/build.gradle index 96c0ec188..f211234fa 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/build.gradle +++ b/codeit-bootcamp-spring/1-sprint-mission/build.gradle @@ -1,32 +1,36 @@ plugins { id 'java' + id 'org.springframework.boot' version '3.4.0' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.sprint.mission' -version = '1.0-SNAPSHOT' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} repositories { mavenCentral() } dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' - - // https://mvnrepository.com/artifact/org.mockito/mockito-core - testImplementation 'org.mockito:mockito-core:4.11.0' - // https://mvnrepository.com/artifact/org.assertj/assertj-core - testImplementation 'org.assertj:assertj-core:3.24.2' - - // https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator - implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' - // https://mvnrepository.com/artifact/org.glassfish.expressly/expressly - implementation 'org.glassfish.expressly:expressly:5.0.0' - // https://mvnrepository.com/artifact/com.google.guava/guava - implementation 'com.google.guava:guava:33.2.1-jre' - + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } -test { +tasks.named('test') { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/gradle/wrapper/gradle-wrapper.jar b/codeit-bootcamp-spring/1-sprint-mission/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 zcma&N1CXTcmMvW9vTb(Rwr$&4wr$(C?dmSu>@vG-+vuvg^_??!{yS%8zW-#zn-LkA z5&1^$^{lnmUON?}LBF8_K|(?T0Ra(xUH{($5eN!MR#ZihR#HxkUPe+_R8Cn`RRs(P z_^*#_XlXmGv7!4;*Y%p4nw?{bNp@UZHv1?Um8r6)Fei3p@ClJn0ECfg1hkeuUU@Or zDaPa;U3fE=3L}DooL;8f;P0ipPt0Z~9P0)lbStMS)ag54=uL9ia-Lm3nh|@(Y?B`; zx_#arJIpXH!U{fbCbI^17}6Ri*H<>OLR%c|^mh8+)*h~K8Z!9)DPf zR2h?lbDZQ`p9P;&DQ4F0sur@TMa!Y}S8irn(%d-gi0*WxxCSk*A?3lGh=gcYN?FGl z7D=Js!i~0=u3rox^eO3i@$0=n{K1lPNU zwmfjRVmLOCRfe=seV&P*1Iq=^i`502keY8Uy-WNPwVNNtJFx?IwAyRPZo2Wo1+S(xF37LJZ~%i)kpFQ3Fw=mXfd@>%+)RpYQLnr}B~~zoof(JVm^^&f zxKV^+3D3$A1G;qh4gPVjhrC8e(VYUHv#dy^)(RoUFM?o%W-EHxufuWf(l*@-l+7vt z=l`qmR56K~F|v<^Pd*p~1_y^P0P^aPC##d8+HqX4IR1gu+7w#~TBFphJxF)T$2WEa zxa?H&6=Qe7d(#tha?_1uQys2KtHQ{)Qco)qwGjrdNL7thd^G5i8Os)CHqc>iOidS} z%nFEDdm=GXBw=yXe1W-ShHHFb?Cc70+$W~z_+}nAoHFYI1MV1wZegw*0y^tC*s%3h zhD3tN8b=Gv&rj}!SUM6|ajSPp*58KR7MPpI{oAJCtY~JECm)*m_x>AZEu>DFgUcby z1Qaw8lU4jZpQ_$;*7RME+gq1KySGG#Wql>aL~k9tLrSO()LWn*q&YxHEuzmwd1?aAtI zBJ>P=&$=l1efe1CDU;`Fd+_;&wI07?V0aAIgc(!{a z0Jg6Y=inXc3^n!U0Atk`iCFIQooHqcWhO(qrieUOW8X(x?(RD}iYDLMjSwffH2~tB z)oDgNBLB^AJBM1M^c5HdRx6fBfka`(LD-qrlh5jqH~);#nw|iyp)()xVYak3;Ybik z0j`(+69aK*B>)e_p%=wu8XC&9e{AO4c~O1U`5X9}?0mrd*m$_EUek{R?DNSh(=br# z#Q61gBzEpmy`$pA*6!87 zSDD+=@fTY7<4A?GLqpA?Pb2z$pbCc4B4zL{BeZ?F-8`s$?>*lXXtn*NC61>|*w7J* z$?!iB{6R-0=KFmyp1nnEmLsA-H0a6l+1uaH^g%c(p{iT&YFrbQ$&PRb8Up#X3@Zsk zD^^&LK~111%cqlP%!_gFNa^dTYT?rhkGl}5=fL{a`UViaXWI$k-UcHJwmaH1s=S$4 z%4)PdWJX;hh5UoK?6aWoyLxX&NhNRqKam7tcOkLh{%j3K^4Mgx1@i|Pi&}<^5>hs5 zm8?uOS>%)NzT(%PjVPGa?X%`N2TQCKbeH2l;cTnHiHppPSJ<7y-yEIiC!P*ikl&!B z%+?>VttCOQM@ShFguHVjxX^?mHX^hSaO_;pnyh^v9EumqSZTi+#f&_Vaija0Q-e*| z7ulQj6Fs*bbmsWp{`auM04gGwsYYdNNZcg|ph0OgD>7O}Asn7^Z=eI>`$2*v78;sj-}oMoEj&@)9+ycEOo92xSyY344^ z11Hb8^kdOvbf^GNAK++bYioknrpdN>+u8R?JxG=!2Kd9r=YWCOJYXYuM0cOq^FhEd zBg2puKy__7VT3-r*dG4c62Wgxi52EMCQ`bKgf*#*ou(D4-ZN$+mg&7$u!! z-^+Z%;-3IDwqZ|K=ah85OLwkO zKxNBh+4QHh)u9D?MFtpbl)us}9+V!D%w9jfAMYEb>%$A;u)rrI zuBudh;5PN}_6J_}l55P3l_)&RMlH{m!)ai-i$g)&*M`eN$XQMw{v^r@-125^RRCF0 z^2>|DxhQw(mtNEI2Kj(;KblC7x=JlK$@78`O~>V!`|1Lm-^JR$-5pUANAnb(5}B}JGjBsliK4& zk6y(;$e&h)lh2)L=bvZKbvh@>vLlreBdH8No2>$#%_Wp1U0N7Ank!6$dFSi#xzh|( zRi{Uw%-4W!{IXZ)fWx@XX6;&(m_F%c6~X8hx=BN1&q}*( zoaNjWabE{oUPb!Bt$eyd#$5j9rItB-h*5JiNi(v^e|XKAj*8(k<5-2$&ZBR5fF|JA z9&m4fbzNQnAU}r8ab>fFV%J0z5awe#UZ|bz?Ur)U9bCIKWEzi2%A+5CLqh?}K4JHi z4vtM;+uPsVz{Lfr;78W78gC;z*yTch~4YkLr&m-7%-xc ztw6Mh2d>_iO*$Rd8(-Cr1_V8EO1f*^@wRoSozS) zy1UoC@pruAaC8Z_7~_w4Q6n*&B0AjOmMWa;sIav&gu z|J5&|{=a@vR!~k-OjKEgPFCzcJ>#A1uL&7xTDn;{XBdeM}V=l3B8fE1--DHjSaxoSjNKEM9|U9#m2<3>n{Iuo`r3UZp;>GkT2YBNAh|b z^jTq-hJp(ebZh#Lk8hVBP%qXwv-@vbvoREX$TqRGTgEi$%_F9tZES@z8Bx}$#5eeG zk^UsLBH{bc2VBW)*EdS({yw=?qmevwi?BL6*=12k9zM5gJv1>y#ML4!)iiPzVaH9% zgSImetD@dam~e>{LvVh!phhzpW+iFvWpGT#CVE5TQ40n%F|p(sP5mXxna+Ev7PDwA zamaV4m*^~*xV+&p;W749xhb_X=$|LD;FHuB&JL5?*Y2-oIT(wYY2;73<^#46S~Gx| z^cez%V7x$81}UWqS13Gz80379Rj;6~WdiXWOSsdmzY39L;Hg3MH43o*y8ibNBBH`(av4|u;YPq%{R;IuYow<+GEsf@R?=@tT@!}?#>zIIn0CoyV!hq3mw zHj>OOjfJM3F{RG#6ujzo?y32m^tgSXf@v=J$ELdJ+=5j|=F-~hP$G&}tDZsZE?5rX ztGj`!S>)CFmdkccxM9eGIcGnS2AfK#gXwj%esuIBNJQP1WV~b~+D7PJTmWGTSDrR` zEAu4B8l>NPuhsk5a`rReSya2nfV1EK01+G!x8aBdTs3Io$u5!6n6KX%uv@DxAp3F@{4UYg4SWJtQ-W~0MDb|j-$lwVn znAm*Pl!?Ps&3wO=R115RWKb*JKoexo*)uhhHBncEDMSVa_PyA>k{Zm2(wMQ(5NM3# z)jkza|GoWEQo4^s*wE(gHz?Xsg4`}HUAcs42cM1-qq_=+=!Gk^y710j=66(cSWqUe zklbm8+zB_syQv5A2rj!Vbw8;|$@C!vfNmNV!yJIWDQ>{+2x zKjuFX`~~HKG~^6h5FntRpnnHt=D&rq0>IJ9#F0eM)Y-)GpRjiN7gkA8wvnG#K=q{q z9dBn8_~wm4J<3J_vl|9H{7q6u2A!cW{bp#r*-f{gOV^e=8S{nc1DxMHFwuM$;aVI^ zz6A*}m8N-&x8;aunp1w7_vtB*pa+OYBw=TMc6QK=mbA-|Cf* zvyh8D4LRJImooUaSb7t*fVfih<97Gf@VE0|z>NcBwBQze);Rh!k3K_sfunToZY;f2 z^HmC4KjHRVg+eKYj;PRN^|E0>Gj_zagfRbrki68I^#~6-HaHg3BUW%+clM1xQEdPYt_g<2K+z!$>*$9nQ>; zf9Bei{?zY^-e{q_*|W#2rJG`2fy@{%6u0i_VEWTq$*(ZN37|8lFFFt)nCG({r!q#9 z5VK_kkSJ3?zOH)OezMT{!YkCuSSn!K#-Rhl$uUM(bq*jY? zi1xbMVthJ`E>d>(f3)~fozjg^@eheMF6<)I`oeJYx4*+M&%c9VArn(OM-wp%M<-`x z7sLP1&3^%Nld9Dhm@$3f2}87!quhI@nwd@3~fZl_3LYW-B?Ia>ui`ELg z&Qfe!7m6ze=mZ`Ia9$z|ARSw|IdMpooY4YiPN8K z4B(ts3p%2i(Td=tgEHX z0UQ_>URBtG+-?0E;E7Ld^dyZ;jjw0}XZ(}-QzC6+NN=40oDb2^v!L1g9xRvE#@IBR zO!b-2N7wVfLV;mhEaXQ9XAU+>=XVA6f&T4Z-@AX!leJ8obP^P^wP0aICND?~w&NykJ#54x3_@r7IDMdRNy4Hh;h*!u(Ol(#0bJdwEo$5437-UBjQ+j=Ic>Q2z` zJNDf0yO6@mr6y1#n3)s(W|$iE_i8r@Gd@!DWDqZ7J&~gAm1#~maIGJ1sls^gxL9LLG_NhU!pTGty!TbhzQnu)I*S^54U6Yu%ZeCg`R>Q zhBv$n5j0v%O_j{QYWG!R9W?5_b&67KB$t}&e2LdMvd(PxN6Ir!H4>PNlerpBL>Zvyy!yw z-SOo8caEpDt(}|gKPBd$qND5#a5nju^O>V&;f890?yEOfkSG^HQVmEbM3Ugzu+UtH zC(INPDdraBN?P%kE;*Ae%Wto&sgw(crfZ#Qy(<4nk;S|hD3j{IQRI6Yq|f^basLY; z-HB&Je%Gg}Jt@={_C{L$!RM;$$|iD6vu#3w?v?*;&()uB|I-XqEKqZPS!reW9JkLewLb!70T7n`i!gNtb1%vN- zySZj{8-1>6E%H&=V}LM#xmt`J3XQoaD|@XygXjdZ1+P77-=;=eYpoEQ01B@L*a(uW zrZeZz?HJsw_4g0vhUgkg@VF8<-X$B8pOqCuWAl28uB|@r`19DTUQQsb^pfqB6QtiT z*`_UZ`fT}vtUY#%sq2{rchyfu*pCg;uec2$-$N_xgjZcoumE5vSI{+s@iLWoz^Mf; zuI8kDP{!XY6OP~q5}%1&L}CtfH^N<3o4L@J@zg1-mt{9L`s^z$Vgb|mr{@WiwAqKg zp#t-lhrU>F8o0s1q_9y`gQNf~Vb!F%70f}$>i7o4ho$`uciNf=xgJ>&!gSt0g;M>*x4-`U)ysFW&Vs^Vk6m%?iuWU+o&m(2Jm26Y(3%TL; zA7T)BP{WS!&xmxNw%J=$MPfn(9*^*TV;$JwRy8Zl*yUZi8jWYF>==j~&S|Xinsb%c z2?B+kpet*muEW7@AzjBA^wAJBY8i|#C{WtO_or&Nj2{=6JTTX05}|H>N2B|Wf!*3_ z7hW*j6p3TvpghEc6-wufFiY!%-GvOx*bZrhZu+7?iSrZL5q9}igiF^*R3%DE4aCHZ zqu>xS8LkW+Auv%z-<1Xs92u23R$nk@Pk}MU5!gT|c7vGlEA%G^2th&Q*zfg%-D^=f z&J_}jskj|Q;73NP4<4k*Y%pXPU2Thoqr+5uH1yEYM|VtBPW6lXaetokD0u z9qVek6Q&wk)tFbQ8(^HGf3Wp16gKmr>G;#G(HRBx?F`9AIRboK+;OfHaLJ(P>IP0w zyTbTkx_THEOs%Q&aPrxbZrJlio+hCC_HK<4%f3ZoSAyG7Dn`=X=&h@m*|UYO-4Hq0 z-Bq&+Ie!S##4A6OGoC~>ZW`Y5J)*ouaFl_e9GA*VSL!O_@xGiBw!AF}1{tB)z(w%c zS1Hmrb9OC8>0a_$BzeiN?rkPLc9%&;1CZW*4}CDDNr2gcl_3z+WC15&H1Zc2{o~i) z)LLW=WQ{?ricmC`G1GfJ0Yp4Dy~Ba;j6ZV4r{8xRs`13{dD!xXmr^Aga|C=iSmor% z8hi|pTXH)5Yf&v~exp3o+sY4B^^b*eYkkCYl*T{*=-0HniSA_1F53eCb{x~1k3*`W zr~};p1A`k{1DV9=UPnLDgz{aJH=-LQo<5%+Em!DNN252xwIf*wF_zS^!(XSm(9eoj z=*dXG&n0>)_)N5oc6v!>-bd(2ragD8O=M|wGW z!xJQS<)u70m&6OmrF0WSsr@I%T*c#Qo#Ha4d3COcX+9}hM5!7JIGF>7<~C(Ear^Sn zm^ZFkV6~Ula6+8S?oOROOA6$C&q&dp`>oR-2Ym3(HT@O7Sd5c~+kjrmM)YmgPH*tL zX+znN>`tv;5eOfX?h{AuX^LK~V#gPCu=)Tigtq9&?7Xh$qN|%A$?V*v=&-2F$zTUv z`C#WyIrChS5|Kgm_GeudCFf;)!WH7FI60j^0o#65o6`w*S7R@)88n$1nrgU(oU0M9 zx+EuMkC>(4j1;m6NoGqEkpJYJ?vc|B zOlwT3t&UgL!pX_P*6g36`ZXQ; z9~Cv}ANFnJGp(;ZhS(@FT;3e)0)Kp;h^x;$*xZn*k0U6-&FwI=uOGaODdrsp-!K$Ac32^c{+FhI-HkYd5v=`PGsg%6I`4d9Jy)uW0y%) zm&j^9WBAp*P8#kGJUhB!L?a%h$hJgQrx!6KCB_TRo%9{t0J7KW8!o1B!NC)VGLM5! zpZy5Jc{`r{1e(jd%jsG7k%I+m#CGS*BPA65ZVW~fLYw0dA-H_}O zrkGFL&P1PG9p2(%QiEWm6x;U-U&I#;Em$nx-_I^wtgw3xUPVVu zqSuKnx&dIT-XT+T10p;yjo1Y)z(x1fb8Dzfn8e yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 60756 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 /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +134,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + 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=SC2039,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=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# 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. + +# 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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +217,12 @@ set -- \ 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. diff --git a/codeit-bootcamp-spring/1-sprint-mission/gradlew.bat b/codeit-bootcamp-spring/1-sprint-mission/gradlew.bat index 107acd32c..9d21a2183 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/gradlew.bat +++ b/codeit-bootcamp-spring/1-sprint-mission/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +59,11 @@ 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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 diff --git a/codeit-bootcamp-spring/1-sprint-mission/settings.gradle b/codeit-bootcamp-spring/1-sprint-mission/settings.gradle index b71004546..2437dfb29 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/settings.gradle +++ b/codeit-bootcamp-spring/1-sprint-mission/settings.gradle @@ -1,2 +1 @@ -rootProject.name = '1-sprint-mission' - +rootProject.name = 'discodeit' diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java new file mode 100644 index 000000000..dbea429e2 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DiscodeitApplication { + + public static void main(String[] args) { + SpringApplication.run(DiscodeitApplication.class, args); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties new file mode 100644 index 000000000..545cf9f09 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=discodeit diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java new file mode 100644 index 000000000..7ab8d98cb --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DiscodeitApplicationTests { + + @Test + void contextLoads() { + } + +} From f74a9434e0663698abff2b46b20fd24b060e7640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:12:39 +0900 Subject: [PATCH 02/38] =?UTF-8?q?feat:=20=EA=B8=B0=EB=B3=B8=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1-sprint-mission/README.md | 99 ++++++++++++++++++- .../1-sprint-mission/build.gradle | 12 +++ .../discodeit/DiscodeitApplication.java | 68 ++++++++++++- .../discodeit/entity/channel/Channel.java | 28 ++---- .../entity/common/AbstractUUIDEntity.java | 31 ++---- .../entity/message/ChannelMessage.java | 15 +-- .../entity/message/DirectMessage.java | 32 +----- .../user/entity/ParticipatedChannel.java | 1 + .../discodeit/entity/user/entity/User.java | 30 ++---- .../entity/user/entity/UserName.java | 8 +- .../file/FileAbstractRepository.java | 28 ++---- .../file/channel/FileChannelRepository.java | 5 +- .../message/FileChannelMessageRepository.java | 8 +- .../message/FileDirectMessageRepository.java | 4 +- .../file/user/FileUserRepository.java | 2 + .../basic/BasicChannelMessageService.java | 16 +-- .../service/basic/BasicChannelService.java | 14 +-- .../basic/BasicDirectMessageService.java | 14 +-- .../service/basic/BasicUserService.java | 12 +-- .../service/channel/ChannelConverter.java | 4 +- .../converter/ChannelMessageConverter.java | 2 + .../converter/DirectMessageConverter.java | 2 + .../discodeit/service/user/UserConverter.java | 7 +- ...pplication.properties => application.yaml} | 0 .../entity/common/AbstractUUIDTest.java | 6 +- 25 files changed, 263 insertions(+), 185 deletions(-) rename codeit-bootcamp-spring/1-sprint-mission/src/main/resources/{application.properties => application.yaml} (100%) diff --git a/codeit-bootcamp-spring/1-sprint-mission/README.md b/codeit-bootcamp-spring/1-sprint-mission/README.md index b27dfc5e9..4e7efe866 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/README.md +++ b/codeit-bootcamp-spring/1-sprint-mission/README.md @@ -2,4 +2,101 @@ - Java 프로젝트를 Spring 프로젝트로 마이그레이션 - 의존성 관리를 IoC Container에 위임하도록 리팩토링 -- 비즈니스 로직 고도화 \ No newline at end of file +- 비즈니스 로직 고도화 + 기본 요구사항 + Spring 프로젝트 초기화 + - [x] Spring Initializr를 통해 zip 파일을 다운로드하세요. + - [x] 빌드 시스템은 Gradle - Groovy를 사용합니다. + - [x] 언어는 Java 17를 사용합니다. + - [x] Spring Boot의 버전은 3.4.0입니다. + - [x] GroupId는 com.sprint.mission입니다. + - [x] ArtifactId와 Name은 discodeit입니다. + - [x] packaging 형식은 Jar입니다 + - [x] Dependency를 추가합니다. + - [x] Lombok + - [x] Spring Web + - [x] zip 파일을 압축해제하고 원래 진행 중이던 프로젝트에 붙여넣기하세요. 일부 파일은 덮어쓰기할 수 있습니다. + - [x] application.properties 파일을 yaml 형식으로 변경하세요. + - [x] DiscodeitApplication의 main 메서드를 실행하고 로그를 확인해보세요. + + +--- + +Bean 선언 및 테스트 +- [x] File*Repository 구현체를 Repository 인터페이스의 Bean으로 등록하세요. +- [x] Basic*Service 구현체를 Service 인터페이스의 Bean으로 등록하세요. +- [x] JavaApplication에서 테스트했던 코드를 DiscodeitApplication에서 테스트해보세요. + - [x] JavaApplication 의 main 메소드를 제외한 모든 메소드를 DiscodeitApplication클래스로 복사하세요. + - [x] JavaApplication의 main 메소드에서 Service를 초기화하는 코드를 Spring Context를 활용하여 대체하세요. + +```java +// JavaApplication +public static void main(String[] args) { + // 레포지토리 초기화 + // ... + // 서비스 초기화 + UserService userService = new BasicUserService(userRepository); + ChannelService channelService = new BasicChannelService(channelRepository); + MessageService messageService = new BasicMessageService(messageRepository, channelRepository, userRepository); + + // ... +} + +// DiscodeitApplication +public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(DiscodeitApplication.class, args); + // 서비스 초기화 + // TODO context에서 Bean을 조회하여 각 서비스 구현체 할당 코드 작성하세요. + UserService userService; + ChannelService channelService; + MessageService messageService; + + // ... +} +``` + + - [x] JavaApplication의 main 메소드의 셋업, 테스트 부분의 코드를 DiscodeitApplication클래스로 복사하세요. +```java +public static void main(String[] args) { + // ... + // 셋업 + User user = setupUser(userService); + Channel channel = setupChannel(channelService); + // 테스트 + messageCreateTest(messageService, channel, user); +} +``` + +Spring 핵심 개념 이해하기 +- [ ] JavaApplication과 DiscodeitApplication에서 Service를 초기화하는 방식의 차이에 대해 다음의 키워드를 중심으로 정리해보세요. +IoC Container +Dependency Injection +Bean + + +Lombok 적용 +- [x] 도메인 모델의 getter 메소드를 @Getter로 대체해보세요. +- [x] Basic*Service의 생성자를 @RequiredArgsConstructor로 대체해보세요. + + +비즈니스 로직 고도화 +- [ ] 다음의 기능 요구 사항을 구현하세요. + +추가 기능 요구사항 +시간 타입 변경하기 +- [ ] 시간을 다루는 필드의 타입은 Instant로 통일합니다. + - 기존에 사용하던 Long보다 가독성이 뛰어나며, 시간대(Time Zone) 변환과 정밀한 시간 연산이 가능해 확장성이 높습니다. + +**새로운 도메인 추가하기** +- [ ] 공통: 앞서 정의한 도메인 모델과 동일하게 공통 필드(id, createdAt, updatedAt)를 포함합니다. +- [ ] ReadStatus + - 사용자가 채널 별 마지막으로 메시지를 읽은 시간을 표현하는 도메인 모델입니다. 사용자별 각 채널에 읽지 않은 메시지를 확인하기 위해 활용합니다. +- [ ] UserStatus + - 사용자 별 마지막으로 확인된 접속 시간을 표현하는 도메인 모델입니다. 사용자의 온라인 상태를 확인하기 위해 활용합니다. + - [ ] 마지막 접속 시간을 기준으로 현재 로그인한 유저로 판단할 수 있는 메소드를 정의하세요. 마지막 접속 시간이 현재 시간으로부터 5분 이내이면 현재 접속 중인 유저로 간주합니다. +- [ ] BinaryContent + - 이미지, 파일 등 바이너리 데이터를 표현하는 도메인 모델입니다. 사용자의 프로필 이미지, 메시지에 첨부된 파일을 저장하기 위해 활용합니다. + - [ ] 수정 불가능한 도메인 모델로 간주합니다. 따라서 updatedAt 필드는 정의하지 않습니다. + - [ ] User, Message 도메인 모델과의 의존 관계 방향성을 잘 고려하여 id 참조 필드를 추가하세요. +- [ ] 각 도메인 모델 별 레포지토리 인터페이스를 선언하세요. +레포지토리 구현체(File, JCF)는 아직 구현하지 마세요. 이어지는 서비스 고도화 요구사항에 따라 레포지토리 인터페이스에 메소드가 추가될 수 있어요. \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/build.gradle b/codeit-bootcamp-spring/1-sprint-mission/build.gradle index f211234fa..040627196 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/build.gradle +++ b/codeit-bootcamp-spring/1-sprint-mission/build.gradle @@ -29,6 +29,18 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // https://mvnrepository.com/artifact/org.mockito/mockito-core + testImplementation 'org.mockito:mockito-core:4.11.0' + // https://mvnrepository.com/artifact/org.assertj/assertj-core + testImplementation 'org.assertj:assertj-core:3.24.2' + + // https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator + implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' + // https://mvnrepository.com/artifact/org.glassfish.expressly/expressly + implementation 'org.glassfish.expressly:expressly:5.0.0' + // https://mvnrepository.com/artifact/com.google.guava/guava + implementation 'com.google.guava:guava:33.2.1-jre' } tasks.named('test') { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java index dbea429e2..d6d51329f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java @@ -1,13 +1,79 @@ package com.sprint.mission.discodeit; +import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; +import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; +import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; +import com.sprint.mission.discodeit.entity.message.dto.SendDirectMessageRequest; +import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; +import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; +import com.sprint.mission.discodeit.repository.file.channel.FileChannelRepository; +import com.sprint.mission.discodeit.repository.file.message.FileDirectMessageRepository; +import com.sprint.mission.discodeit.repository.file.user.FileUserRepository; +import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; +import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; +import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; +import com.sprint.mission.discodeit.service.channel.ChannelService; +import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; +import com.sprint.mission.discodeit.service.user.UserService; +import java.util.UUID; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class DiscodeitApplication { + private static UserService userService; + private static ChannelService channelService; + private static DirectMessageService directMessageService; public static void main(String[] args) { - SpringApplication.run(DiscodeitApplication.class, args); + ConfigurableApplicationContext appContext = SpringApplication.run(DiscodeitApplication.class, args); + userService = appContext.getBean(UserService.class); + channelService = appContext.getBean(ChannelService.class); + directMessageService = appContext.getBean(DirectMessageService.class); + + UserInfoResponse user = registerUser(userService); + ChannelResponse channelResponse = registerChannel(channelService, user.uuid()); + UserInfoResponse destinationUser = registerUser(userService); + DirectMessageInfoResponse sendDirectMessage = sendDirectMessage(directMessageService,user.uuid(), destinationUser.uuid()); + + saveToFile(appContext); + } + + private static UserInfoResponse registerUser(UserService userService) { + var registerRequest = new RegisterUserRequest("홍길동"); + return userService.register(registerRequest); + } + + private static ChannelResponse registerChannel(ChannelService channelService, UUID userId) { + var channelCreateRequest = new CreateNewChannelRequest(userId, "스프링부트_1기"); + return channelService.createChannelOrThrow(channelCreateRequest); + } + + private static DirectMessageInfoResponse sendDirectMessage(DirectMessageService directMessageService, UUID sendUserId, UUID receiveUserId) { + var sendDirectMessageRequest = new SendDirectMessageRequest(sendUserId, receiveUserId, "안녕하세요"); + return directMessageService.sendMessage(sendDirectMessageRequest); } + private static void saveToFile(ConfigurableApplicationContext appContext) { + saveChannelToFile(appContext); + saveUserToFile(appContext); + saveDirectMessageToFile(appContext); + } + + private static void saveUserToFile(ConfigurableApplicationContext appContext) { + FileUserRepository fileUserRepository = (FileUserRepository) appContext.getBean(UserRepository.class); + fileUserRepository.saveToFile(); + } + + private static void saveChannelToFile(ConfigurableApplicationContext appContext) { + FileChannelRepository fileChannelRepository = (FileChannelRepository) appContext.getBean(ChannelRepository.class); + fileChannelRepository.saveToFile(); + } + + private static void saveDirectMessageToFile(ConfigurableApplicationContext appContext) { + FileDirectMessageRepository fileDirectMessageRepository = + (FileDirectMessageRepository) appContext.getBean(DirectMessageRepository.class); + fileDirectMessageRepository.saveToFile(); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java index 93b312c97..e54da0b2a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java @@ -7,7 +7,9 @@ import com.sprint.mission.discodeit.entity.user.entity.User; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.Getter; +@Getter public class Channel extends AbstractUUIDEntity { @NotNull @@ -19,18 +21,6 @@ public class Channel extends AbstractUUIDEntity { private final User creator; - /** - * ==> 코드 리뷰 받고 싶은 부분 - * 필드에 참여한 유저의 객체를 참조하고 있는 자료구조를 추가하고 싶습니다. ex) private ParticipatedUser participatedUsers - * 생각되는 문제는 순환참조로 어떻게 끊어줄 수 있을까입니다 - * 1. userId 만 저장하고 있는 자료구조 -> 데이터베이스식은 객체지향 코딩에서 거리가 먼것 같아 저는 지양합니다. - * 2. User 객체를 담은 자료구조 -> 유저가 소유한 채널 중 해당 채널을 찾아, 채널에 대한 정보를 참조한다. - * -> 채널에 참가한 유저 객체를 참조한다 .. 무한루프.. - * 3. 생각되는 문제점 : 디비는 스프링부트로 기능이 있는것으로 알지만, 파일 시스템으로 직접 구현할 경우 막막함, - * 일단 해보지 않았지만, 어떤 방식으로 생각해야하는지 ? - */ - - private Channel(String channelName, User creator) { this.channelName = channelName; this.creator = creator; @@ -66,17 +56,13 @@ public boolean isStatusNotUnregisteredAndEqualsTo(String channelName) { return isNotUnregistered() && this.channelName.equals(channelName); } - public String getChannelName() { - return channelName; - } - private void checkCreatorEqualsOrThrow(User user) { var isNotCreator = isNotCreator(user); if (isNotCreator) { throw ChannelException.ofErrorMessageAndCreatorName( ErrorMessage.CHANNEL_NOT_EQUAL_CREATOR, - user.getName() + user.getUserName() ); } } @@ -86,8 +72,8 @@ private boolean isNotCreator(User user) { return !creator.equals(user); } - public String getCreator() { - return creator.getName(); + public String getCreatorName() { + return creator.getUserName(); } @Override @@ -97,8 +83,8 @@ public String toString() { "channel info = [channel name = %s creator = %s, createAt = %d, updateAt = %d, status = %s]", channelName, creator.getName(), - getCreateAt(), - getUpdateAt().orElse(0L), + getCreateAt().toEpochMilli(), + getUpdateAt().toEpochMilli(), getStatus().toString() ); return format; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java index 2e0d90a7f..9ef9d85bc 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java @@ -7,45 +7,30 @@ import com.google.common.base.Preconditions; import java.io.Serial; import java.io.Serializable; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Optional; +import java.time.Instant; import java.util.UUID; +import lombok.Getter; +@Getter public abstract class AbstractUUIDEntity implements Serializable { @Serial private static final long serialVersionUID = -2898060967687082469L; private final UUID id; - private final Long createAt; + private final Instant createAt; - private Long updateAt = null; + private Instant updateAt; private Status status; protected AbstractUUIDEntity() { this.id = UUID.randomUUID(); this.createAt = createUnixTimestamp(); + this.updateAt = createUnixTimestamp(); this.status = REGISTERED; } - public Optional getUpdateAt() { - return Optional.ofNullable(updateAt); - } - - public UUID getId() { - return id; - } - - public Long getCreateAt() { - return createAt; - } - - public Status getStatus() { - return status; - } - private void updateStatus(Status status) { Preconditions.checkNotNull(status); this.status = status; @@ -64,8 +49,8 @@ public boolean isNotUnregistered() { return status != UNREGISTERED; } - private long createUnixTimestamp() { - return LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + private Instant createUnixTimestamp() { + return Instant.now(); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java index d8354c4d5..8bc8197d4 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java @@ -3,7 +3,9 @@ import com.sprint.mission.discodeit.entity.channel.Channel; import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; import com.sprint.mission.discodeit.entity.user.entity.User; +import lombok.Getter; +@Getter public class ChannelMessage extends AbstractUUIDEntity { private final Sender sender; @@ -41,19 +43,6 @@ public static ChannelMessage ofMessageAndSenderAndReceiverChannel( return new ChannelMessage(channelSender, message, messageSender, receiverChannel); } - public String getMessage() { - return message; - } - - public User getMessageSender() { - return messageSender; - } - - public Channel getReceiverChannel() { - return receiverChannel; - } - - // 채널에 속한 모든 사람들에게 알림을 전송해야겠네 ? public void sendMessage() { sender.sendMessage(messageSender, receiverChannel, message); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java index d0489d76a..396c88110 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java @@ -2,10 +2,12 @@ import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; import com.sprint.mission.discodeit.entity.user.entity.User; +import lombok.Getter; +@Getter public class DirectMessage extends AbstractUUIDEntity { // 보내는 방법을 추상화 - private final Sender sender; + private final transient Sender sender; private final String message; @@ -37,30 +39,4 @@ public void sendMessage() { sender.sendMessage(messageSender, messageReceiver, message); } - public String getMessage() { - return message; - } - - public User getMessageSender() { - return messageSender; - } - - public User getMessageReceiver() { - return messageReceiver; - } -} -/** - * 메세지를 주고 받는 행위를 어떻게 처리해주어야 할까? - * 1. 유저가 메시지를 발송한다. - * 2. 애플리케이션에서 메시지를 저장한다. - * 3. 애플리케이션에서 메시지 알림을 전송한다. - * 4. 받은 유저가 알림을 확인하고, 메시지를 확인한다. 메시지를 확인할 때 기존의 주고받은 메시지도 모두 들고 와야함 => 서비스 레포에서 처리해야할 일 같음 - */ - -/** - * 메시지가 저장할 데이터는 ? - * 1. 메시지 원문 - * 2. 보낸 사람 - * 3. 받는 사람 - * etc. 메시지 원문 말고, 사진(url 포함), 다양한 것을 보낼 수 있는데 저장할지 확실하지 않은 데이터를 필드로 가지고 있어야하는가? - */ \ No newline at end of file +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java index b5edbf355..12d051197 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import lombok.Getter; public class ParticipatedChannel implements Serializable { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java index e783ae631..bf7ec82a9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java @@ -4,9 +4,12 @@ import com.google.common.base.Preconditions; import com.sprint.mission.discodeit.entity.channel.Channel; import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import java.time.Instant; import java.util.List; import java.util.UUID; +import lombok.Getter; +@Getter public class User extends AbstractUUIDEntity { private UserName name; @@ -32,17 +35,6 @@ public void changeUserName(String newName) { updateStatusAndUpdateAt(); } - /** - * 유저에게 채널을 생성하는 책임이 있는 것이아니라, - * 다른 객체에 해당 작업을 위임, 그런데 메서드 명이 영... - * 이 부분 코드 리뷰 부탁드릴게요! - */ - - /** - * 유저의 참여 채널에 새로운 채널을 생성하는 메서드 - * @param channelName - * @return - */ public Channel openNewChannel(String channelName) { Preconditions.checkNotNull(channelName); var createdChannel = participatedChannels.createChannel(channelName, this); @@ -57,10 +49,6 @@ public Channel changeChannelName(UUID channelId, String channelName) { return targetChannel; } - /** - * 참여한 채널에서 나가는 메서드 - * @param channelId - */ public void exitParticipatedChannel(UUID channelId) { participatedChannels.exitChannelById(channelId); } @@ -74,14 +62,12 @@ public List getParticipatedChannels() { return participatedChannels.findAllChannels(); } - public String getName() { - return name.getName(); - } - public void unregister() { updateUnregistered(); } - + public String getUserName() { + return name.getName(); + } @Override public String toString() { var format = @@ -90,8 +76,8 @@ public String toString() { getId(), getName(), getStatus().toString(), - getCreateAt(), - getUpdateAt().orElse(0L), + getCreateAt().toEpochMilli(), + getUpdateAt().toEpochMilli(), getParticipatedChannels() ); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java index a2838e354..9c3e7282a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java @@ -7,7 +7,9 @@ import java.io.Serial; import java.io.Serializable; import java.util.Objects; +import lombok.Getter; +@Getter public class UserName implements Serializable { public static final int NAME_MIN_LENGTH = 3; @@ -19,7 +21,7 @@ public class UserName implements Serializable { min = NAME_MIN_LENGTH, max = NAME_MAX_LENGTH, message = "'${validatedValue}' must be between {min} and {max} characters long" ) - @NotNull //TODO : => NotBlank , 테스트 코드 수정 후 변경 + @NotNull private final String name; public UserName(String name) { @@ -36,10 +38,6 @@ public UserName changeName(String name) { return new UserName(name); } - public String getName() { - return name; - } - @Override public String toString() { return name; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java index a90c6ad84..4f1e2d0b8 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java @@ -15,10 +15,9 @@ import java.util.UUID; public abstract class FileAbstractRepository - extends InMemoryCrudRepository - implements Closeable { + extends InMemoryCrudRepository { - private File file; + private final File file; protected FileAbstractRepository(String filePath) { file = new File(filePath); @@ -34,16 +33,14 @@ protected Map loadFile() { FileInputStream fis = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(fis); ) { - while (true) { - try { - var readUser = (T) ois.readObject(); - store.put(readUser.getId(), readUser); - } catch (ClassNotFoundException e) { - System.out.println(e.getMessage()); - } + try { + store = (Map) ois.readObject(); + } catch (ClassNotFoundException e) { + System.out.println(e.getMessage()); } } catch (IOException e) { - System.out.println("User loading exit"); + System.out.println("loading exit"); + throw new IllegalArgumentException(); } return store; } @@ -54,18 +51,11 @@ public void saveToFile() { FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos); ) { - - for (T value : store.values()) { - oos.writeObject(value); - } + oos.writeObject(store); } catch (IOException e) { throw new RuntimeException(e); } } - @Override - public void close() throws IOException { - saveToFile(); - } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java index 49b2e2033..5995be58a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java @@ -4,9 +4,12 @@ import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; import com.sprint.mission.discodeit.repository.file.FileAbstractRepository; import java.util.UUID; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; +@Repository public class FileChannelRepository extends FileAbstractRepository implements ChannelRepository { - private static final String CHANNEL_FILE_PATH_NAME = "temp/file/user/channel.ser"; + private static final String CHANNEL_FILE_PATH_NAME = "temp/file/channel/channel.ser"; private static ChannelRepository FILE_USER_REPOSITORY_INSTANCE; protected FileChannelRepository() { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java index 1ba6fd4a1..55ca813c2 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java @@ -4,7 +4,9 @@ import com.sprint.mission.discodeit.repository.file.FileAbstractRepository; import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; import java.util.UUID; +import org.springframework.stereotype.Repository; +@Repository public class FileChannelMessageRepository extends FileAbstractRepository implements ChannelMessageRepository { @@ -12,14 +14,14 @@ public class FileChannelMessageRepository extends FileAbstractRepository implements DirectMessageRepository { - private static final String DIRECT_MESSAGE_FILE_PATH_NAME = "temp/file/user/channel.ser"; + private static final String DIRECT_MESSAGE_FILE_PATH_NAME = "temp/file/message/directmessage.ser"; private static FileDirectMessageRepository FILE_DIRECT_REPOSITORY_INSTANCE; protected FileDirectMessageRepository() { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java index bef1ebc48..3d7677e13 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java @@ -5,7 +5,9 @@ import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; import java.util.Optional; import java.util.UUID; +import org.springframework.stereotype.Repository; +@Repository public class FileUserRepository extends FileAbstractRepository implements UserRepository { private static final String FILE_PATH_USER_NAME = "temp/file/user/user.ser"; private static UserRepository FILE_USER_REPOSITORY_INSTANCE; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java index 8a3c613e2..276a0c165 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java @@ -14,7 +14,11 @@ import com.sprint.mission.discodeit.service.message.channelMessage.ChannelMessageService; import com.sprint.mission.discodeit.service.message.converter.ChannelMessageConverter; import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +@Service +@RequiredArgsConstructor public class BasicChannelMessageService implements ChannelMessageService { private final UserRepository userRepository; @@ -22,18 +26,6 @@ public class BasicChannelMessageService implements ChannelMessageService { private final ChannelMessageRepository channelMessageRepository; private final ChannelMessageConverter converter; - private BasicChannelMessageService( - UserRepository userRepository, - ChannelRepository channelRepository, - ChannelMessageRepository channelMessageRepository, - ChannelMessageConverter converter - ) { - this.userRepository = userRepository; - this.channelRepository = channelRepository; - this.channelMessageRepository = channelMessageRepository; - this.converter = converter; - } - public static ChannelMessageService getInstance( UserRepository userRepository, ChannelRepository channelRepository, diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java index 2ffd1e9c0..80095c8bd 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java @@ -15,23 +15,17 @@ import com.sprint.mission.discodeit.service.channel.ChannelConverter; import com.sprint.mission.discodeit.service.channel.ChannelService; import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +@Service +@RequiredArgsConstructor public class BasicChannelService implements ChannelService { private final ChannelRepository channelRepository; private final UserRepository userRepository; private final ChannelConverter channelConverter; - private BasicChannelService( - ChannelRepository channelRepository, - UserRepository userRepository, - ChannelConverter channelConverter - ) { - this.channelRepository = channelRepository; - this.userRepository = userRepository; - this.channelConverter = channelConverter; - } - public static ChannelService getInstance(UserRepository userRepository, ChannelRepository channelRepository) { return new BasicChannelService(channelRepository, userRepository, new ChannelConverter()); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java index 08d8ed5f4..4d1507865 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java @@ -11,23 +11,17 @@ import com.sprint.mission.discodeit.service.message.converter.DirectMessageConverter; import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +@Service +@RequiredArgsConstructor public class BasicDirectMessageService implements DirectMessageService { private final DirectMessageRepository directMessageRepository; private final UserRepository userRepository; private final DirectMessageConverter directMessageConverter; - private BasicDirectMessageService( - DirectMessageRepository directMessageRepository, - UserRepository userRepository, - DirectMessageConverter directMessageConverter - ) { - this.directMessageRepository = directMessageRepository; - this.userRepository = userRepository; - this.directMessageConverter = directMessageConverter; - } - public static DirectMessageService getInstance( DirectMessageRepository directMessageRepository, UserRepository userRepository diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java index c5ce66fa7..c4370364b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java @@ -14,20 +14,16 @@ import com.sprint.mission.discodeit.service.user.UserConverter; import com.sprint.mission.discodeit.service.user.UserService; import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +@Service +@RequiredArgsConstructor public class BasicUserService implements UserService { private final UserRepository userRepository; private final UserConverter converter; - public BasicUserService( - UserRepository userRepository, - UserConverter converter - ) { - this.userRepository = userRepository; - this.converter = converter; - } - @Override public UserInfoResponse register(RegisterUserRequest registerUserRequest) { var entity = converter.toEntity(registerUserRequest); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java index bd0badab8..31a0307d1 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java @@ -3,14 +3,16 @@ import com.sprint.mission.discodeit.entity.channel.Channel; import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse.Builder; +import org.springframework.stereotype.Component; +@Component public class ChannelConverter { public ChannelResponse toDto(Channel channel) { var channelResponse = new Builder() .channelId(channel.getId()) .channelName(channel.getChannelName()) - .creator(channel.getCreator()) + .creator(channel.getCreatorName()) .status(channel.getStatus().getStatus()) .build(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java index 2deed9290..da9d6f802 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java @@ -3,7 +3,9 @@ import com.sprint.mission.discodeit.entity.message.ChannelMessage; import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse; import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse.Builder; +import org.springframework.stereotype.Component; +@Component public class ChannelMessageConverter { public ChannelMessageInfoResponse toDto(ChannelMessage message) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java index dda0676a5..869eeb796 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java @@ -3,7 +3,9 @@ import com.sprint.mission.discodeit.entity.message.DirectMessage; import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse.Builder; +import org.springframework.stereotype.Component; +@Component public class DirectMessageConverter { public DirectMessageInfoResponse toDto(DirectMessage directMessage) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java index 47977d732..3a1adff01 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java @@ -4,11 +4,14 @@ import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; import com.sprint.mission.discodeit.entity.user.entity.User; import java.util.Objects; +import org.springframework.stereotype.Component; +@Component public class UserConverter { private static UserConverter INSTANCE; - public UserConverter() {} + public UserConverter() { + } public static UserConverter getInstance() { if (INSTANCE == null) { @@ -25,7 +28,7 @@ public User toEntity(RegisterUserRequest request) { public UserInfoResponse toDto(User user) { var responseDto = new UserInfoResponse( user.getId(), - user.getName(), + user.getUserName(), user.getStatus() ); return responseDto; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml similarity index 100% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties rename to codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java index ce2b9c836..b84fb900d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java @@ -30,8 +30,7 @@ void createAbstractUUIDThenInitializeFieldIdAndCreatedAtAndStatusIsNotNullTest() assertAll( () -> assertThat(entity.getId()).as("Id should not be null").isNotNull(), () -> assertThat(entity.getCreateAt()).as("createAt should not be null").isNotNull(), - () -> assertThat(entity.getStatus()).isEqualTo(REGISTERED), - () -> assertThat(entity.getUpdateAt()).as("At Initialized updateAt time is must be Empty").isEmpty() + () -> assertThat(entity.getStatus()).isEqualTo(REGISTERED) ); } @@ -39,12 +38,11 @@ void createAbstractUUIDThenInitializeFieldIdAndCreatedAtAndStatusIsNotNullTest() @DisplayName("엔티티 내 데이터가 수정 시 updateAt and status 필드 수정 여부 테스트") void givenWhenSomeDateModifyThenFieldUpdateAtAndStatusIsChangedTest() { // given - assertThat(entity.getUpdateAt()).isEmpty(); // when entity.updateStatusAndUpdateAt(); //then assertAll( - () -> assertThat(entity.getUpdateAt()).as("invoke update() then updateAt must be present").isPresent(), + () -> assertThat(entity.getUpdateAt()).as("invoke update() then updateAt must be present"), () -> assertThat(entity.getStatus()).isEqualTo(MODIFIED) ); } From 425357bc049621a116989448c12adb68e2e99b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:17:26 +0900 Subject: [PATCH 03/38] =?UTF-8?q?refactor=20:=20user=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EC=9D=98=20=ED=95=84=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mission/discodeit/domain/user/Email.java | 19 ++++++ .../discodeit/domain/user/Nickname.java | 20 ++++++ .../user/validation/EmailValidator.java | 22 +++++++ .../user/validation/NicknameValidator.java | 23 +++++++ .../entity/binarycontent/BinaryContent.java | 22 +++++++ .../discodeit/entity/channel/Channel.java | 8 +-- ...bstractUUIDEntity.java => BaseEntity.java} | 6 +- .../entity/message/ChannelMessage.java | 4 +- .../entity/message/DirectMessage.java | 4 +- .../entity/readstatus/ReadStatus.java | 32 ++++++++++ .../discodeit/entity/user/entity/User.java | 43 +++++-------- .../entity/userstatus/UserStatus.java | 32 ++++++++++ .../common/InMemoryCrudRepository.java | 4 +- .../file/FileAbstractRepository.java | 5 +- .../jcf/user/JCFUserRepositoryInMemory.java | 2 +- .../discodeit/service/user/UserConverter.java | 2 +- .../discodeit/DiscodeitApplicationTests.java | 4 -- .../entity/common/AbstractUUIDTest.java | 8 +-- .../discodeit/entity/user/UserTest.java | 15 ----- .../entity/user/domain/EmailTest.java | 23 +++++++ .../entity/user/domain/NicknameTest.java | 35 +++++++++++ .../user/entity/ParticipatedChannelTest.java | 2 +- .../discodeit/testdummy/TestUUIDEntity.java | 4 +- .../1-sprint-mission/study/Valid.md | 59 ++++++++++++++++++ .../1-sprint-mission/study/domain.png | Bin 0 -> 46725 bytes .../1-sprint-mission/study/domaintest.png | Bin 0 -> 57304 bytes .../1-sprint-mission/study/refactoring.md | 12 ++++ 27 files changed, 338 insertions(+), 72 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/{AbstractUUIDEntity.java => BaseEntity.java} (91%) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/study/Valid.md create mode 100644 codeit-bootcamp-spring/1-sprint-mission/study/domain.png create mode 100644 codeit-bootcamp-spring/1-sprint-mission/study/domaintest.png create mode 100644 codeit-bootcamp-spring/1-sprint-mission/study/refactoring.md diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java new file mode 100644 index 000000000..f8d1cdbe3 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java @@ -0,0 +1,19 @@ +package com.sprint.mission.discodeit.domain.user; + +import com.sprint.mission.discodeit.domain.user.validation.EmailValidator; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@EqualsAndHashCode(of = {"value"}) +@ToString(of = {"value"}) +public class Email { + + private final String value; + + public Email(String email) { + EmailValidator.valid(email); + this.value = email; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java new file mode 100644 index 000000000..9b488f3dd --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.domain.user; + +import com.sprint.mission.discodeit.domain.user.validation.NicknameValidator; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@EqualsAndHashCode(of = {"value"}) +@ToString(of = {"value"}) +public class Nickname { + + private final String value; + + public Nickname(String value) { + NicknameValidator.validate(value); + this.value = value; + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java new file mode 100644 index 000000000..7959b3655 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java @@ -0,0 +1,22 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import java.util.regex.Pattern; +import org.springframework.util.Assert; + +public class EmailValidator { + + private final static String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\." + "[a-zA-Z0-9_+&*-]+)*@" + "(?:[a-zA-Z0-9-]+\\.)+[a-z" + "A-Z]{2,7}$"; + private final static Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + + + public static void valid(String email) { + Assert.notNull(email, "이메일은 필수입니다."); + if (email.isBlank()) { + throw new IllegalArgumentException("이메일은 필수입니다."); + } + + if (!EMAIL_PATTERN.matcher(email).matches()) { + throw new IllegalArgumentException("이메일 형식이 틀렸습니다 : 입력 = " + email); + } + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java new file mode 100644 index 000000000..96afad98a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import org.springframework.util.Assert; + +public class NicknameValidator { + private static final int MAX_LENGTH = 15; + private static final int MIN_LENGTH = 1; + private static final String LENGTH_ERROR_MESSAGE = "닉네임은 " + MIN_LENGTH + " ~ " + MAX_LENGTH + "제한입니다. : 입력한 이름 길이 = "; + + public static void validate(String value) { + Assert.notNull(value, "닉네임은 필수입니다."); + + if (value.isBlank() || value.length() > MAX_LENGTH || value.length() < MIN_LENGTH) { + throw new IllegalArgumentException(provideLengthNameErrorMessage(value)); + } + } + + // TODO 에러 관리 따로 관리하기. + private static String provideLengthNameErrorMessage(String value) { + return LENGTH_ERROR_MESSAGE + value.length(); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java new file mode 100644 index 000000000..9ea3a147a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java @@ -0,0 +1,22 @@ +package com.sprint.mission.discodeit.entity.binarycontent; + +import java.io.File; +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; +import lombok.Getter; + +@Getter +public class BinaryContent { + private final UUID id; + private final Instant createdAt; + private final File file; + + public BinaryContent(File file) { + Objects.requireNonNull(file, "file is null"); + this.id = UUID.randomUUID(); + this.createdAt = Instant.now(); + this.file = file; + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java index e54da0b2a..3db158c01 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java @@ -3,14 +3,14 @@ import com.google.common.base.Preconditions; import com.sprint.mission.discodeit.common.error.ErrorMessage; import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import com.sprint.mission.discodeit.entity.common.BaseEntity; import com.sprint.mission.discodeit.entity.user.entity.User; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Getter; @Getter -public class Channel extends AbstractUUIDEntity { +public class Channel extends BaseEntity { @NotNull @Size( @@ -62,7 +62,7 @@ private void checkCreatorEqualsOrThrow(User user) { if (isNotCreator) { throw ChannelException.ofErrorMessageAndCreatorName( ErrorMessage.CHANNEL_NOT_EQUAL_CREATOR, - user.getUserName() + user.getNicknameValue() ); } } @@ -73,7 +73,7 @@ private boolean isNotCreator(User user) { } public String getCreatorName() { - return creator.getUserName(); + return creator.getNicknameValue(); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java similarity index 91% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java index 9ef9d85bc..f68cf9782 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDEntity.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java @@ -12,7 +12,7 @@ import lombok.Getter; @Getter -public abstract class AbstractUUIDEntity implements Serializable { +public abstract class BaseEntity implements Serializable { @Serial private static final long serialVersionUID = -2898060967687082469L; @@ -24,7 +24,7 @@ public abstract class AbstractUUIDEntity implements Serializable { private Status status; - protected AbstractUUIDEntity() { + protected BaseEntity() { this.id = UUID.randomUUID(); this.createAt = createUnixTimestamp(); this.updateAt = createUnixTimestamp(); @@ -61,7 +61,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - AbstractUUIDEntity that = (AbstractUUIDEntity) o; + BaseEntity that = (BaseEntity) o; return id.equals(that.id); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java index 8bc8197d4..0f252ebef 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java @@ -1,12 +1,12 @@ package com.sprint.mission.discodeit.entity.message; import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import com.sprint.mission.discodeit.entity.common.BaseEntity; import com.sprint.mission.discodeit.entity.user.entity.User; import lombok.Getter; @Getter -public class ChannelMessage extends AbstractUUIDEntity { +public class ChannelMessage extends BaseEntity { private final Sender sender; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java index 396c88110..aa2404042 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java @@ -1,11 +1,11 @@ package com.sprint.mission.discodeit.entity.message; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import com.sprint.mission.discodeit.entity.common.BaseEntity; import com.sprint.mission.discodeit.entity.user.entity.User; import lombok.Getter; @Getter -public class DirectMessage extends AbstractUUIDEntity { +public class DirectMessage extends BaseEntity { // 보내는 방법을 추상화 private final transient Sender sender; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java new file mode 100644 index 000000000..bf0a0ce23 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java @@ -0,0 +1,32 @@ +package com.sprint.mission.discodeit.entity.readstatus; + +import com.sprint.mission.discodeit.entity.user.entity.User; +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; +import lombok.Getter; + +@Getter +public class ReadStatus { + + private final UUID id; + private final Instant createdAt; + private Instant updatedAt; + private boolean isRead; + private final User user; + + public ReadStatus(User user) { + Objects.requireNonNull(user, "user is null"); + this.id = UUID.randomUUID(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + this.isRead = false; + this.user = user; + } + + public void updateReadStatusByReading() { + this.isRead = true; + this.updatedAt = Instant.now(); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java index bf7ec82a9..fe3a782fa 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java @@ -3,35 +3,35 @@ import com.google.common.base.Preconditions; import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; -import java.time.Instant; +import com.sprint.mission.discodeit.entity.common.BaseEntity; +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.Nickname; import java.util.List; import java.util.UUID; import lombok.Getter; +import lombok.ToString; @Getter -public class User extends AbstractUUIDEntity { - - private UserName name; +@ToString +public class User extends BaseEntity { + private Nickname name; + private Email email; private final ParticipatedChannel participatedChannels; - private User(UserName name, ParticipatedChannel channel) { + private User(Nickname name, ParticipatedChannel channel) { this.name = name; this.participatedChannels = channel; } public static User createFrom(String username) { - Preconditions.checkNotNull(username); - var userName = UserName.createFrom(username); + var userName = new Nickname(username); var participatedChannel = ParticipatedChannel.newDefault(); return new User(userName, participatedChannel); } public void changeUserName(String newName) { - Preconditions.checkNotNull(newName); - var changedName = this.name.changeName(newName); - this.name = changedName; + this.name = new Nickname(newName); updateStatusAndUpdateAt(); } @@ -65,23 +65,10 @@ public List getParticipatedChannels() { public void unregister() { updateUnregistered(); } - public String getUserName() { - return name.getName(); - } - @Override - public String toString() { - var format = - String.format( - "user info = [id : %s, name: %s, status : %s, createAt = %d, updateAt = %d], participatedChannel = {%s}", - getId(), - getName(), - getStatus().toString(), - getCreateAt().toEpochMilli(), - getUpdateAt().toEpochMilli(), - getParticipatedChannels() - ); - - return format; + + public String getNicknameValue() { + return name.getValue(); } + } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java new file mode 100644 index 000000000..c887a4689 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java @@ -0,0 +1,32 @@ +package com.sprint.mission.discodeit.entity.userstatus; + +import com.sprint.mission.discodeit.entity.user.entity.User; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.UUID; +import lombok.Getter; + +@Getter +public class UserStatus { + + private final UUID id; + private Instant createdAt; + private Instant updatedAt; + private final User user; + + public UserStatus(User user) { + Objects.requireNonNull(user, "user is null"); + this.id = UUID.randomUUID(); + this.user = user; + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + } + public void updateConnectTime() { + updatedAt = Instant.now(); + } + public boolean isActiveWithInFiveMinutes() { + Instant withInFiveMin = Instant.now().minus(5, ChronoUnit.MINUTES); + return updatedAt.isAfter(withInFiveMin); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java index 3e3b659a7..ee9058ab0 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.repository.common; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import com.sprint.mission.discodeit.entity.common.BaseEntity; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -9,7 +9,7 @@ import java.util.Optional; import java.util.UUID; -public abstract class InMemoryCrudRepository +public abstract class InMemoryCrudRepository implements CrudRepository { protected final Map store = new HashMap<>(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java index 4f1e2d0b8..7de412af8 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java @@ -1,8 +1,7 @@ package com.sprint.mission.discodeit.repository.file; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import com.sprint.mission.discodeit.entity.common.BaseEntity; import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -14,7 +13,7 @@ import java.util.Map; import java.util.UUID; -public abstract class FileAbstractRepository +public abstract class FileAbstractRepository extends InMemoryCrudRepository { private final File file; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java index dd48cb4b8..d64751eac 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java @@ -14,7 +14,7 @@ private JCFUserRepositoryInMemory() {} @Override public Optional findByUsername(String username) { var findUser = findAll().stream() - .filter(user -> username.equals(user.getName())) + .filter(user -> username.equals(user.getNicknameValue())) .findFirst(); return findUser; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java index 3a1adff01..224217f7d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java @@ -28,7 +28,7 @@ public User toEntity(RegisterUserRequest request) { public UserInfoResponse toDto(User user) { var responseDto = new UserInfoResponse( user.getId(), - user.getUserName(), + user.getNicknameValue(), user.getStatus() ); return responseDto; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java index 7ab8d98cb..1c51df754 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java @@ -6,8 +6,4 @@ @SpringBootTest class DiscodeitApplicationTests { - @Test - void contextLoads() { - } - } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java index b84fb900d..c0e565839 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java @@ -16,7 +16,7 @@ public class AbstractUUIDTest { - private AbstractUUIDEntity entity; + private BaseEntity entity; @BeforeEach @@ -51,8 +51,8 @@ void givenWhenSomeDateModifyThenFieldUpdateAtAndStatusIsChangedTest() { @DisplayName("동일한 UUID를 가진 객체간의 동등성 비교를 할 경우 True를 반환하는지 테스트") void givenCreateAbstractEntityAndEqualEntityWhenIsEqualsThenReturnTrueTest() throws Exception { // given - AbstractUUIDEntity entity1 = new TestUUIDEntity(); - AbstractUUIDEntity entity2 = new TestUUIDEntity(); + BaseEntity entity1 = new TestUUIDEntity(); + BaseEntity entity2 = new TestUUIDEntity(); UUID sameUUID = UUID.randomUUID(); @@ -62,7 +62,7 @@ void givenCreateAbstractEntityAndEqualEntityWhenIsEqualsThenReturnTrueTest() thr // then assertEquals(entity1, entity2, "Entities with the same UUID should be equal"); - AbstractUUIDEntity entity3 = new TestUUIDEntity(); + BaseEntity entity3 = new TestUUIDEntity(); assertNotEquals(entity1, entity3, "Entities with different UUIDs should not be equal"); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java index 53f017c7f..198cef97b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java @@ -34,21 +34,6 @@ void setUp() { user = User.createFrom(USER_NAME); } - @Test - @DisplayName("유저 객체를 생성한 후 객체의 name value 와 초기화 시 입력 값과 동일 테스트") - void givenUserWhenGetUserNameThenReturnUserName() { - // given - - // when - var userName = user.getName(); - // then - assertThat(userName).isEqualTo(USER_NAME); - } - - // TODO : 유저 이름 수정 성공 테스트 - - // TODO : 유저 이름 수정 실패 테스트 - @Test @DisplayName("유저 해지 요청 후 유저의 상태 변경") void givenUnregisterRequestWhenUnregisterThenStatusChange() { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java new file mode 100644 index 000000000..9792ff08f --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.entity.user.domain; + +import static org.assertj.core.api.Assertions.*; + +import com.sprint.mission.discodeit.domain.user.Email; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class EmailTest { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"test.com"}) + void 잘못된_이메일_형식_생성시_에러throw(String email) { + //given + // when + Throwable catchThrow = catchThrowable(() -> new Email(email)); + // then + assertThat(catchThrow).isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java new file mode 100644 index 000000000..5b83dfd2c --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java @@ -0,0 +1,35 @@ +package com.sprint.mission.discodeit.entity.user.domain; + +import static org.assertj.core.api.Assertions.*; + +import com.sprint.mission.discodeit.domain.user.Nickname; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class NicknameTest { + + @ParameterizedTest + @NullAndEmptySource + void 유저_닉네임_생성_null값으로생성_에러throw(String value) { + //given + // when + Throwable catchThrowable = catchThrowable(() -> new Nickname(value)); + // then + assertThat(catchThrowable).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testMethodNameHere() { + //given + String nickname = "test"; + Nickname nickname1 = new Nickname(nickname); + Nickname nickname2 = new Nickname(nickname); + // when + boolean result = nickname1.equals(nickname2); + + // then + assertThat(result).isTrue(); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java index 2f8d0e221..ba64897d3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java @@ -197,7 +197,7 @@ void givenNotChannelCreatorWhenChangeChannelNameThenThrowChannelException() { // then assertThat(throwable).isInstanceOf(ChannelException.class) - .hasMessageContaining(notCreatorUser.getName()); + .hasMessageContaining(notCreatorUser.getNicknameValue()); } @Test diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java index 114ac72bf..2fd8a5f6a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.testdummy; -import com.sprint.mission.discodeit.entity.common.AbstractUUIDEntity; +import com.sprint.mission.discodeit.entity.common.BaseEntity; -public class TestUUIDEntity extends AbstractUUIDEntity { +public class TestUUIDEntity extends BaseEntity { } diff --git a/codeit-bootcamp-spring/1-sprint-mission/study/Valid.md b/codeit-bootcamp-spring/1-sprint-mission/study/Valid.md new file mode 100644 index 000000000..f7528155b --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/study/Valid.md @@ -0,0 +1,59 @@ +# 도메인 유효성 검증 + +도메인의 유효성 검증은 비즈니스 로직으로 적절히 처리되고 에러를 반환해주어야 한다. +그렇다면, 어떻게 검증하는 것이 좋을까 ? + +우선, 도메인 객체와 DTO 객체의 분리를 먼저 하자. 도메인 객체에서 던지는 에러와 DTO 에서 던지는 +에러를 분리해서 생각해본다는 말이다. 밀접하게 연관되어 있는 두 객체 사이의 비슷한 검증으로 인해 +어떻게 만들어야 할지 헷갈리기 때문이다. + +도메인 객체의 유효성 검사를 위해서 외부에 validator 담당 객체를 만들어 준다. 그리고 도메인 +객체의 생성자에서 정적 메서드를 호출하여 적절한 처리를 통해 도메인의 무결성을 보장한다. + +분리를 통해서 만들어지는 `검증 객체`가 많아질 수 도 있겠다고 생각한다. 공통되는 부분이 있다면 +최대한 고려해서 리팩터링을 진행하는 것이 중요하다. + +또한 검증객체에서 던지는 `커스텀 에러 객체` 또한 만들어주어야 한다. 이를 통해서 공통되는 +에러 객체를 던지고 그 내용은 `검증 객체`에 따라서 다른 내용이 들어가게 만들어준다. 이후 던져진 +에러를 잡고 적절히 반환해주기 위해 컨트롤러에서 응답객체에 적절히 담아서 사용자에게 응답한다. + +## Hibernate Validator(@Valid) 사용 시 커스텀 Validator 가 필요 없는가? +hibernate 에서 제공해주는 valid 기능을 사용하게 된다면 내 도메인 객체에서 값을 검증해주는 +검증 객체 또는 검증 메서드를 만들 필요가 없지 않을까라고 생각이 들었습니다. + +결론은 커스텀 검증 객체가 필요합니다. 그 이유에 대해서 살펴보겠습니다. + +우선 다음 코드가 테스트 실패합니다. + +

+ 도메인 + 테스트 +
+ +### Hibernate Validator(@Valid) 는 언제 동작하는가? +Hibernate validator 가 동작하는 경우는 다음과 같습니다. +1. Spring MVC에서 요청 바인딩 시 (@Valid 사용) +2. JPA 엔티티로 저장할 때 유효성 검사를 실행할 때 +3. 명시적으로 Hibernate Validator를 호출할 때 + +즉, 엔티티 도메인 객체 생성자에서 `new Test("123")` 처럼 직접 객체를 만드는 과정에서는 +검증이 되지 않습니다. 왜냐하면 `@Email` 검증이 동작하지 않기 때문입니다. + +
+ +| 상황 | Hibernate Validator(@Valid) 자동 실행 여부 | +|------|--------------------------------| +| **Spring 컨트롤러에서 `@Valid` 사용** | ✅ 자동 실행됨 | +| **JPA에서 `persist()` 또는 `update()` 호출 시** | ✅ 자동 실행됨 (단, `@PrePersist`, `@PreUpdate` 필요) | +| **`new Test("123")`로 직접 객체 생성** | ❌ 자동 실행되지 않음 | + +### 엔티티 도메인 객체에 필요할까? +또 한가지 생각해볼것이 있습니다. Validator 기능을 통해서 검증을 실행하면 되는 것이 아닌가요. +왜 도메인 커스텀 검증 객체를 만들어야 하는지 이유는 무엇일까요? + +JPA의 기능으로 데이터베이스에 저장할 때 검증을 하게 된다면 영속성 컨텍스트에서 검증 후 +DB 요청을 하기 때문에 별다른 검증을 진행하지 않아도 되지 않을까 생각이 듭니다. 만약 JPA를 +지원하지 않는 데이터베이스로 변경하게 된다면 정상적으로 검증이 실행되지 않을 것입니다. 그렇다면 +수동으로 Hibernate Validator 를 실행시켜야 하는데, 코드가 매우 지저분해질 것입니다. + +이러한 이유 때문에 값을 검증하는 로직을 관리하는 객체를 만들고 검증을 실행해야 한다고 생각합니다. diff --git a/codeit-bootcamp-spring/1-sprint-mission/study/domain.png b/codeit-bootcamp-spring/1-sprint-mission/study/domain.png new file mode 100644 index 0000000000000000000000000000000000000000..6d515bbba86495770b948b7e0821091396c31508 GIT binary patch literal 46725 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�#atA#`0Yzx#>=G#-goE>MdD#K>Y9md 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

Z{`5v$k|y3L+?N82 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 Date: Thu, 6 Feb 2025 18:42:31 +0900 Subject: [PATCH 04/38] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=80=EA=B3=A0=20=EB=8B=A4=EC=8B=9C=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84,=20=EC=9C=A0=EC=A0=80=EC=9D=98=20=EC=9D=B4=EB=A6=84,?= =?UTF-8?q?=20=EB=8B=89=EB=84=A4=EC=9E=84,=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유저 이름,닉네임, 이메일 비즈니스 로직인 유효성 검증 --- .../1-sprint-mission/build.gradle | 11 +- .../discodeit/DiscodeitApplication.java | 65 +----- .../mission/discodeit/JavaApplication.java | 40 ---- .../discodeit/common/error/ErrorMessage.java | 36 --- .../error/channel/ChannelException.java | 42 ---- .../common/error/user/UserException.java | 47 ---- .../AppendableObjectOutputStream.java | 40 ---- .../common/valid/ValidationUtils.java | 25 -- .../discodeit/config/factory/AppFactory.java | 29 --- .../factory/ApplicationFileFactory.java | 100 -------- .../factory/ApplicationInMemoryFactory.java | 97 -------- .../discodeit/domain/user/BirthDate.java | 16 ++ .../mission/discodeit/domain/user/Email.java | 2 +- .../discodeit/domain/user/Nickname.java | 2 +- .../discodeit/domain/user/Password.java | 14 ++ .../mission/discodeit/domain/user/User.java | 91 ++++++++ .../discodeit/domain/user/Username.java | 20 ++ .../user/enums/EmailSubscriptionStatus.java | 6 + .../user/exception/EmailInvalidException.java | 12 + .../exception/NickNameInvalidException.java | 11 + .../exception/PassWordInvalidException.java | 11 + .../exception/UserNameInvalidException.java | 11 + .../user/validation/EmailValidator.java | 19 +- .../user/validation/NicknameValidator.java | 32 ++- .../user/validation/UsernameValidator.java | 42 ++++ .../entity/binarycontent/BinaryContent.java | 22 -- .../discodeit/entity/channel/Channel.java | 92 -------- .../channel/dto/ChangeChannelNameRequest.java | 6 - .../entity/channel/dto/ChannelResponse.java | 48 ---- .../channel/dto/CreateNewChannelRequest.java | 13 -- .../channel/dto/DeleteChannelRequest.java | 6 - .../discodeit/entity/common/BaseEntity.java | 72 ------ .../discodeit/entity/common/Status.java | 19 -- .../entity/message/ChannelMessage.java | 50 ---- .../entity/message/DirectMessage.java | 42 ---- .../discodeit/entity/message/Sender.java | 6 - .../dto/ChannelMessageInfoResponse.java | 37 --- .../dto/DirectMessageInfoResponse.java | 37 --- .../dto/SendChannelMessageRequest.java | 7 - .../message/dto/SendDirectMessageRequest.java | 6 - .../entity/readstatus/ReadStatus.java | 32 --- .../entity/user/dto/ExitChannelRequest.java | 6 - .../entity/user/dto/FindUserRequest.java | 4 - .../user/dto/ModifyUserInfoRequest.java | 6 - .../entity/user/dto/RegisterUserRequest.java | 4 - .../user/dto/UnregisterUserRequest.java | 6 - .../entity/user/dto/UserInfoResponse.java | 7 - .../user/entity/ParticipatedChannel.java | 99 -------- .../discodeit/entity/user/entity/User.java | 74 ------ .../entity/user/entity/UserName.java | 64 ----- .../entity/userstatus/UserStatus.java | 32 --- .../discodeit/global/error/ErrorCode.java | 48 ++++ .../error/exception/BusinessException.java | 20 ++ .../exception/EntityNotFoundException.java | 10 + .../error/exception/InvalidException.java | 10 + .../repository/common/CrudRepository.java | 19 -- .../common/InMemoryCrudRepository.java | 58 ----- .../repository/common/Repository.java | 4 - .../file/FileAbstractRepository.java | 60 ----- .../file/channel/FileChannelRepository.java | 26 --- .../message/FileChannelMessageRepository.java | 29 --- .../message/FileDirectMessageRepository.java | 26 --- .../file/user/FileUserRepository.java | 36 --- .../jcf/channel/ChannelRepository.java | 9 - .../channel/JCFChannelRepositoryInMemory.java | 15 -- .../ChannelMessageRepository.java | 8 - .../JCFChannelMessageRepositoryInMemory.java | 14 -- .../DirectMessageRepository.java | 9 - .../JCFDirectMessageRepositoryInMemory.java | 16 -- .../jcf/user/JCFUserRepositoryInMemory.java | 35 --- .../repository/jcf/user/UserRepository.java | 16 -- .../basic/BasicChannelMessageService.java | 85 ------- .../service/basic/BasicChannelService.java | 81 ------- .../basic/BasicDirectMessageService.java | 61 ----- .../service/basic/BasicUserService.java | 82 ------- .../service/channel/ChannelConverter.java | 21 -- .../service/channel/ChannelService.java | 14 -- .../service/jcf/JCFChannelMessageService.java | 93 -------- .../service/jcf/JCFChannelService.java | 87 ------- .../service/jcf/JCFDirectMessageService.java | 67 ------ .../discodeit/service/jcf/JCFUserService.java | 86 ------- .../channelMessage/ChannelMessageService.java | 9 - .../converter/ChannelMessageConverter.java | 21 -- .../converter/DirectMessageConverter.java | 21 -- .../directMessage/DirectMessageService.java | 9 - .../discodeit/service/user/UserConverter.java | 36 --- .../discodeit/service/user/UserService.java | 28 --- .../discodeit/DiscodeitApplicationTests.java | 9 - .../sprint/mission/discodeit/UUIDTest.java | 21 -- .../discodeit/domain/user/EmailTest.java | 32 +++ .../discodeit/domain/user/NicknameTest.java | 36 +++ .../discodeit/domain/user/UsernameTest.java | 30 +++ .../user/validation/EmailValidatorTest.java | 39 ++++ .../validation/NicknameValidatorTest.java | 46 ++++ .../validation/UsernameValidatorTest.java | 53 +++++ .../discodeit/entity/channel/ChannelTest.java | 108 --------- .../entity/common/AbstractUUIDTest.java | 75 ------ .../discodeit/entity/user/UserNameTest.java | 98 -------- .../discodeit/entity/user/UserTest.java | 100 -------- .../entity/user/domain/EmailTest.java | 23 -- .../entity/user/domain/NicknameTest.java | 35 --- .../user/entity/ParticipatedChannelTest.java | 220 ------------------ .../common/InMemoryCrudRepositoryTest.java | 136 ----------- .../user/UserRepositoryImplTest.java | 39 ---- .../service/jcf/JCFChannelServiceTest.java | 51 ---- .../service/jcf/JCFUserServiceTest.java | 131 ----------- .../discodeit/testdummy/TestDummyFactory.java | 24 -- .../TestDummyInMemoryCurdRepository.java | 7 - .../discodeit/testdummy/TestUUIDEntity.java | 6 - ...0 \353\252\250\353\215\270\353\247\201.md" | 24 ++ 110 files changed, 618 insertions(+), 3612 deletions(-) delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/ErrorMessage.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/channel/ChannelException.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/user/UserException.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/objectstream/AppendableObjectOutputStream.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/valid/ValidationUtils.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/AppFactory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationFileFactory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationInMemoryFactory.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/enums/EmailSubscriptionStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/EmailInvalidException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/NickNameInvalidException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/PassWordInvalidException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNameInvalidException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChangeChannelNameRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChannelResponse.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/CreateNewChannelRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/DeleteChannelRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/Status.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/Sender.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/ChannelMessageInfoResponse.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/DirectMessageInfoResponse.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendChannelMessageRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendDirectMessageRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ExitChannelRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/FindUserRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ModifyUserInfoRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/RegisterUserRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UnregisterUserRequest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UserInfoResponse.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/EntityNotFoundException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/InvalidException.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/CrudRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/Repository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileDirectMessageRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/ChannelRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/JCFChannelRepositoryInMemory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/ChannelMessageRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/JCFChannelMessageRepositoryInMemory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/DirectMessageRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/JCFDirectMessageRepositoryInMemory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/UserRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelMessageService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFDirectMessageService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFUserService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/channelMessage/ChannelMessageService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/directMessage/DirectMessageService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserService.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/UUIDTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/channel/ChannelTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserNameTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepositoryTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/UserRepositoryImplTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFChannelServiceTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFUserServiceTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyFactory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyInMemoryCurdRepository.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java create mode 100644 "codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" diff --git a/codeit-bootcamp-spring/1-sprint-mission/build.gradle b/codeit-bootcamp-spring/1-sprint-mission/build.gradle index 040627196..f8c023fa7 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/build.gradle +++ b/codeit-bootcamp-spring/1-sprint-mission/build.gradle @@ -30,17 +30,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - // https://mvnrepository.com/artifact/org.mockito/mockito-core - testImplementation 'org.mockito:mockito-core:4.11.0' - // https://mvnrepository.com/artifact/org.assertj/assertj-core - testImplementation 'org.assertj:assertj-core:3.24.2' - // https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' - // https://mvnrepository.com/artifact/org.glassfish.expressly/expressly - implementation 'org.glassfish.expressly:expressly:5.0.0' - // https://mvnrepository.com/artifact/com.google.guava/guava - implementation 'com.google.guava:guava:33.2.1-jre' + + } tasks.named('test') { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java index d6d51329f..658f75ce7 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java @@ -1,79 +1,16 @@ package com.sprint.mission.discodeit; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; -import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendDirectMessageRequest; -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; -import com.sprint.mission.discodeit.repository.file.channel.FileChannelRepository; -import com.sprint.mission.discodeit.repository.file.message.FileDirectMessageRepository; -import com.sprint.mission.discodeit.repository.file.user.FileUserRepository; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; -import com.sprint.mission.discodeit.service.user.UserService; -import java.util.UUID; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class DiscodeitApplication { - private static UserService userService; - private static ChannelService channelService; - private static DirectMessageService directMessageService; + public static void main(String[] args) { ConfigurableApplicationContext appContext = SpringApplication.run(DiscodeitApplication.class, args); - userService = appContext.getBean(UserService.class); - channelService = appContext.getBean(ChannelService.class); - directMessageService = appContext.getBean(DirectMessageService.class); - - UserInfoResponse user = registerUser(userService); - ChannelResponse channelResponse = registerChannel(channelService, user.uuid()); - UserInfoResponse destinationUser = registerUser(userService); - DirectMessageInfoResponse sendDirectMessage = sendDirectMessage(directMessageService,user.uuid(), destinationUser.uuid()); - - saveToFile(appContext); - } - - private static UserInfoResponse registerUser(UserService userService) { - var registerRequest = new RegisterUserRequest("홍길동"); - return userService.register(registerRequest); - } - - private static ChannelResponse registerChannel(ChannelService channelService, UUID userId) { - var channelCreateRequest = new CreateNewChannelRequest(userId, "스프링부트_1기"); - return channelService.createChannelOrThrow(channelCreateRequest); - } - private static DirectMessageInfoResponse sendDirectMessage(DirectMessageService directMessageService, UUID sendUserId, UUID receiveUserId) { - var sendDirectMessageRequest = new SendDirectMessageRequest(sendUserId, receiveUserId, "안녕하세요"); - return directMessageService.sendMessage(sendDirectMessageRequest); } - private static void saveToFile(ConfigurableApplicationContext appContext) { - saveChannelToFile(appContext); - saveUserToFile(appContext); - saveDirectMessageToFile(appContext); - } - - private static void saveUserToFile(ConfigurableApplicationContext appContext) { - FileUserRepository fileUserRepository = (FileUserRepository) appContext.getBean(UserRepository.class); - fileUserRepository.saveToFile(); - } - - private static void saveChannelToFile(ConfigurableApplicationContext appContext) { - FileChannelRepository fileChannelRepository = (FileChannelRepository) appContext.getBean(ChannelRepository.class); - fileChannelRepository.saveToFile(); - } - - private static void saveDirectMessageToFile(ConfigurableApplicationContext appContext) { - FileDirectMessageRepository fileDirectMessageRepository = - (FileDirectMessageRepository) appContext.getBean(DirectMessageRepository.class); - fileDirectMessageRepository.saveToFile(); - } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java deleted file mode 100644 index 77f17a2da..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.sprint.mission.discodeit; - -import com.sprint.mission.discodeit.config.factory.ApplicationFileFactory; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; -import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendDirectMessageRequest; -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; -import java.io.IOException; -import java.util.UUID; - -public class JavaApplication { - private static final ApplicationFileFactory app = ApplicationFileFactory.getInstance(); - - public static void main(String[] args) throws IOException { - UserInfoResponse user = registerUser(); - ChannelResponse channelResponse = registerChannel(user.uuid(), "스프링부트_1기"); - UserInfoResponse receiveMessageUserTemp = registerUser(); - DirectMessageInfoResponse sendDirectMessage = sendDirectMessage(user.uuid(), receiveMessageUserTemp.uuid(), "안녕하세요"); - } - - private static UserInfoResponse registerUser() { - var userService = app.getUserService(); - var registerRequest = new RegisterUserRequest("홍길동"); - return userService.register(registerRequest); - } - - private static ChannelResponse registerChannel(UUID userId, String channelName) { - var channelService = app.getChannelService(); - var channelCreateRequest = new CreateNewChannelRequest(userId, channelName); - return channelService.createChannelOrThrow(channelCreateRequest); - } - - private static DirectMessageInfoResponse sendDirectMessage(UUID sendUserId, UUID receiveUserId, String message) { - var directMessageService = app.getDirectMessageService(); - var sendDirectMessageRequest = new SendDirectMessageRequest(sendUserId, receiveUserId, message); - return directMessageService.sendMessage(sendDirectMessageRequest); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/ErrorMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/ErrorMessage.java deleted file mode 100644 index 600a6e1aa..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/ErrorMessage.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sprint.mission.discodeit.common.error; - -public enum ErrorMessage { - - /** - * 1000 ~ 1999 User 관련 에러 - */ - USER_NOT_FOUND(1_000, "존재하지 않는 유저입니다."), - - USER_NAME_NULL(1_001, "유저의 이름은 반드시 존재해야합니다."), - - NAME_LENGTH_ERROR_MESSAGE(1_002, "유저 이름의 길이 제한을 확인해주세요."), - - USER_NOT_PARTICIPATED_CHANNEL(1_003, "유저가 참여하지 않은 방입니다."), - - /** - * 2000 ~ 2999 Channel 관련 에러 - */ - CHANNEL_NOT_FOUND(2_000, "존재하지 않는 채널입니다."), - CHANNEL_NOT_EQUAL_CREATOR(2_001, "채널을 생성한 사람만 채널이름을 변경할 수 있습니다."), - ; - - private final int errorCode; - private final String message; - - - ErrorMessage(int errorCode, String message) { - this.errorCode = errorCode; - this.message = message; - } - - public String getMessage() { - return message; - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/channel/ChannelException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/channel/ChannelException.java deleted file mode 100644 index 6fbf71914..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/channel/ChannelException.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sprint.mission.discodeit.common.error.channel; - -import com.sprint.mission.discodeit.common.error.ErrorMessage; -import java.util.UUID; - -public class ChannelException extends RuntimeException { - - private ChannelException(String message) { - super(message); - } - - private ChannelException(String message, Throwable cause) { - super(message, cause); - } - - public static ChannelException of(ErrorMessage message) { - return new ChannelException(message.getMessage()); - } - - public static ChannelException ofErrorMessageAndNotExistChannelId(ErrorMessage message, UUID causeInputParameter) { - var format = - String.format( - "%s : input Channel Id = %s", - message.getMessage(), - causeInputParameter.toString() - ); - - return new ChannelException(format); - } - - public static ChannelException ofErrorMessageAndCreatorName( - ErrorMessage message, - String creatorName - ) { - var format = String.format( - "%s : 채널 생성자 %s 가 아닙니다.", - message.getMessage(), creatorName - ); - - return new ChannelException(format); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/user/UserException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/user/UserException.java deleted file mode 100644 index 925572d1b..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/error/user/UserException.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sprint.mission.discodeit.common.error.user; - -import com.sprint.mission.discodeit.common.error.ErrorMessage; - -public class UserException extends RuntimeException { - - public UserException(String message) { - super(message); - } - - public UserException(String message, Throwable cause) { - super(message, cause); - } - - public static UserException of(ErrorMessage message) { - return new UserException(message.getMessage()); - } - - public static UserException of(String message, Throwable cause) { - return new UserException(message, cause); - } - - - public static UserException ofErrorMessageAndId( - ErrorMessage message, String id - ) { - var format = String.format( - "%s : not participated channel id %s", - message.getMessage(), id - ); - - return new UserException(format); - } - - - public static UserException ofErrorMessageAndChannelName( - ErrorMessage message, String channelName - ) { - var format = String.format( - "%s : not participated channel name %s", - message.getMessage(), channelName - ); - - return new UserException(format); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/objectstream/AppendableObjectOutputStream.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/objectstream/AppendableObjectOutputStream.java deleted file mode 100644 index fd6d5ec7d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/objectstream/AppendableObjectOutputStream.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.sprint.mission.discodeit.common.objectstream; - -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.OutputStream; - -public class AppendableObjectOutputStream extends ObjectOutputStream { - - - private boolean append; - private boolean initialized; - private DataOutputStream dout; - - protected AppendableObjectOutputStream(boolean append) throws IOException, SecurityException { - super(); - this.append = append; - this.initialized = true; - } - - public AppendableObjectOutputStream(OutputStream out, boolean append) throws IOException { - super(out); - this.append = append; - this.initialized = true; - this.dout = new DataOutputStream(out); - this.writeStreamHeader(); - } - - @Override - protected void writeStreamHeader() throws IOException { - if (!this.initialized || this.append) return; - if (dout != null) { - dout.writeShort(STREAM_MAGIC); - dout.writeShort(STREAM_VERSION); - } - } -} -/** - * https://stackoverflow.com/questions/1194656/appending-to-an-objectoutputstream - */ diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/valid/ValidationUtils.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/valid/ValidationUtils.java deleted file mode 100644 index 5b9003bdb..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/common/valid/ValidationUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sprint.mission.discodeit.common.valid; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import java.util.Set; - -public class ValidationUtils { - - // TODO hibernate-validator 공식 문서 참조. 응용 하지를 못했음 => 모든 에러 잡아주는 객체 or 서비스 ? 일단 두고 각 객체에 검증 로직 추가해야함 - private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - private static final Validator validator = factory.getValidator(); - - public static void validate(T object) { - Set> violations = validator.validate(object); - if (!violations.isEmpty()) { - StringBuilder errorMessage = new StringBuilder(); - for (ConstraintViolation violation : violations) { - errorMessage.append(violation.getMessage()).append("\n"); - } - throw new IllegalArgumentException(errorMessage.toString()); - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/AppFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/AppFactory.java deleted file mode 100644 index 9ac10981b..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/AppFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sprint.mission.discodeit.config.factory; - -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import com.sprint.mission.discodeit.service.message.channelMessage.ChannelMessageService; -import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; -import com.sprint.mission.discodeit.service.user.UserService; - -public interface AppFactory { - - UserService getUserService(); - - UserRepository getUserRepository(); - - ChannelRepository getChannelRepository(); - - ChannelService getChannelService(); - - DirectMessageService getDirectMessageService(); - - DirectMessageRepository getDirectMessageRepository(); - - ChannelMessageRepository getChannelMessageRepository(); - - ChannelMessageService getChannelMessageService(); -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationFileFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationFileFactory.java deleted file mode 100644 index 5fbd4063d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationFileFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.sprint.mission.discodeit.config.factory; - -import com.sprint.mission.discodeit.repository.file.channel.FileChannelRepository; -import com.sprint.mission.discodeit.repository.file.message.FileChannelMessageRepository; -import com.sprint.mission.discodeit.repository.file.message.FileDirectMessageRepository; -import com.sprint.mission.discodeit.repository.file.user.FileUserRepository; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import com.sprint.mission.discodeit.service.jcf.JCFChannelMessageService; -import com.sprint.mission.discodeit.service.jcf.JCFChannelService; -import com.sprint.mission.discodeit.service.jcf.JCFDirectMessageService; -import com.sprint.mission.discodeit.service.jcf.JCFUserService; -import com.sprint.mission.discodeit.service.message.channelMessage.ChannelMessageService; -import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; -import com.sprint.mission.discodeit.service.user.UserService; - -public class ApplicationFileFactory implements AppFactory { - - private static ApplicationFileFactory INSTANCE; - - - private final UserRepository userRepository; - private final UserService userService; - - private final ChannelRepository channelRepository; - private final ChannelService channelService; - - private final DirectMessageService directMessageService; - private final DirectMessageRepository directMessageRepository; - - private final ChannelMessageService channelMessageService; - private final ChannelMessageRepository channelMessageRepository; - - private static class SingleInstanceHolder { - private static final ApplicationFileFactory INSTANCE = - new ApplicationFileFactory(); - } - - public static synchronized ApplicationFileFactory getInstance() { - return SingleInstanceHolder.INSTANCE; - } - - - private ApplicationFileFactory() { - this.userRepository = FileUserRepository.getInstance(); - this.channelRepository = FileChannelRepository.getInstance(); - this.directMessageRepository = FileDirectMessageRepository.getInstance(); - this.channelMessageRepository = FileChannelMessageRepository.getInstance(); - - this.userService = JCFUserService.getInstance(userRepository); - this.channelService = JCFChannelService.getInstance(userRepository, channelRepository); - this.directMessageService = JCFDirectMessageService.getInstance(directMessageRepository, userRepository); - this.channelMessageService = JCFChannelMessageService.getInstance(userRepository, channelRepository, - channelMessageRepository); - } - - - @Override - public UserService getUserService() { - return userService; - } - - @Override - public UserRepository getUserRepository() { - return userRepository; - } - - @Override - public ChannelRepository getChannelRepository() { - return channelRepository; - } - - @Override - public ChannelService getChannelService() { - return channelService; - } - - @Override - public DirectMessageService getDirectMessageService() { - return directMessageService; - } - - @Override - public DirectMessageRepository getDirectMessageRepository() { - return directMessageRepository; - } - - @Override - public ChannelMessageRepository getChannelMessageRepository() { - return channelMessageRepository; - } - - @Override - public ChannelMessageService getChannelMessageService() { - return channelMessageService; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationInMemoryFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationInMemoryFactory.java deleted file mode 100644 index 2c44fa0af..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/factory/ApplicationInMemoryFactory.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.sprint.mission.discodeit.config.factory; - -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.channel.JCFChannelRepositoryInMemory; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.JCFChannelMessageRepositoryInMemory; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.JCFDirectMessageRepositoryInMemory; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.repository.jcf.user.JCFUserRepositoryInMemory; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import com.sprint.mission.discodeit.service.jcf.JCFChannelMessageService; -import com.sprint.mission.discodeit.service.jcf.JCFChannelService; -import com.sprint.mission.discodeit.service.jcf.JCFDirectMessageService; -import com.sprint.mission.discodeit.service.jcf.JCFUserService; -import com.sprint.mission.discodeit.service.message.channelMessage.ChannelMessageService; -import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; -import com.sprint.mission.discodeit.service.user.UserService; - -/** - * 빈으로 관리되는 스프링과 다르게 정확히 어떻게 구현해야하는지 알기가 어려웠음 - */ -public class ApplicationInMemoryFactory implements AppFactory { - private static ApplicationInMemoryFactory INSTANCE; - - private final UserRepository userRepository; - private final UserService userService; - - private final ChannelRepository channelRepository; - private final ChannelService channelService; - - private final DirectMessageService directMessageService; - private final DirectMessageRepository directMessageRepository; - - private final ChannelMessageService channelMessageService; - private final ChannelMessageRepository channelMessageRepository; - - private static class SingleInstanceHolder { - private static final ApplicationInMemoryFactory INSTANCE = - new ApplicationInMemoryFactory(); - } - private ApplicationInMemoryFactory() { - this.userRepository = JCFUserRepositoryInMemory.getInstance(); - this.channelRepository = JCFChannelRepositoryInMemory.getChannelRepositoryInMemory(); - this.directMessageRepository = JCFDirectMessageRepositoryInMemory.getInstance(); - this.channelMessageRepository = JCFChannelMessageRepositoryInMemory.getInstance(); - - this.userService = JCFUserService.getInstance(userRepository); - this.channelService = JCFChannelService.getInstance(userRepository, channelRepository); - this.directMessageService = JCFDirectMessageService.getInstance(directMessageRepository, userRepository); - this.channelMessageService = JCFChannelMessageService.getInstance(userRepository, channelRepository, channelMessageRepository); - } - - public static synchronized ApplicationInMemoryFactory getInstance() { - return SingleInstanceHolder.INSTANCE; - } - - @Override - public UserService getUserService() { - return userService; - } - - @Override - public ChannelService getChannelService() { - return channelService; - } - - @Override - public UserRepository getUserRepository() { - return userRepository; - } - - @Override - public ChannelRepository getChannelRepository() { - return channelRepository; - } - - @Override - public DirectMessageService getDirectMessageService() { - return directMessageService; - } - - @Override - public DirectMessageRepository getDirectMessageRepository() { - return directMessageRepository; - } - - @Override - public ChannelMessageRepository getChannelMessageRepository() { - return channelMessageRepository; - } - - @Override - public ChannelMessageService getChannelMessageService() { - return channelMessageService; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java new file mode 100644 index 000000000..ff4f2883f --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java @@ -0,0 +1,16 @@ +package com.sprint.mission.discodeit.domain.user; + +import java.time.LocalDate; + +public class BirthDate { + + private final LocalDate value; + + public BirthDate(int year, int month, int day) { + this.value = LocalDate.of(year, month, day); + } + + public LocalDate getValue() { + return value; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java index f8d1cdbe3..7ec6f463b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java @@ -14,6 +14,6 @@ public class Email { public Email(String email) { EmailValidator.valid(email); - this.value = email; + this.value = email.trim(); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java index 9b488f3dd..283f2cb4d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java @@ -14,7 +14,7 @@ public class Nickname { public Nickname(String value) { NicknameValidator.validate(value); - this.value = value; + this.value = value.trim(); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java new file mode 100644 index 000000000..8eb906f79 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java @@ -0,0 +1,14 @@ +package com.sprint.mission.discodeit.domain.user; + +public class Password { + + private final String value; + + public Password(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java new file mode 100644 index 000000000..0d07babfc --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -0,0 +1,91 @@ +package com.sprint.mission.discodeit.domain.user; + +import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public class User { + + private final UUID id; + private final Nickname nickname; + private final Nickname username; + private final Email email; + private final Password password; + private final BirthDate birthDate; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + private final EmailSubscriptionStatus emailSubscriptionStatus; + + public User( + Nickname nickname, + Nickname username, + Email email, + Password password, + BirthDate birthDate, + EmailSubscriptionStatus emailSubscriptionStatus + ) { + this.id = UUID.randomUUID(); + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + this.nickname = nickname; + this.username = username; + this.email = email; + this.password = password; + this.birthDate = birthDate; + this.emailSubscriptionStatus = emailSubscriptionStatus; + } + + public UUID getId() { + return id; + } + + public Nickname getNickname() { + return nickname; + } + + public Email getEmail() { + return email; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public Nickname getUsername() { + return username; + } + + public Password getPassword() { + return password; + } + + public BirthDate getBirthDate() { + return birthDate; + } + + public EmailSubscriptionStatus getEmailSubscriptionStatus() { + return emailSubscriptionStatus; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java new file mode 100644 index 000000000..f4b7d4da1 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.domain.user; + +import com.sprint.mission.discodeit.domain.user.validation.UsernameValidator; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@EqualsAndHashCode(of = "value") +@ToString(of = "value") +public class Username { + + private final String value; + + public Username(String value) { + UsernameValidator.validate(value); + this.value = value.trim(); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/enums/EmailSubscriptionStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/enums/EmailSubscriptionStatus.java new file mode 100644 index 000000000..6c2016b9f --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/enums/EmailSubscriptionStatus.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.domain.user.enums; + +public enum EmailSubscriptionStatus { + SUBSCRIBED, + UNSUBSCRIBED, +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/EmailInvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/EmailInvalidException.java new file mode 100644 index 000000000..6337e6172 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/EmailInvalidException.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class EmailInvalidException extends InvalidException { + + public EmailInvalidException(ErrorCode errorCode, String message) { + super(errorCode, message); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/NickNameInvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/NickNameInvalidException.java new file mode 100644 index 000000000..32609bdd8 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/NickNameInvalidException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class NickNameInvalidException extends InvalidException { + + public NickNameInvalidException(ErrorCode errorCode, String inputValue) { + super(errorCode, inputValue); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/PassWordInvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/PassWordInvalidException.java new file mode 100644 index 000000000..dc899555c --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/PassWordInvalidException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class PassWordInvalidException extends InvalidException { + + public PassWordInvalidException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNameInvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNameInvalidException.java new file mode 100644 index 000000000..28685110a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNameInvalidException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class UserNameInvalidException extends InvalidException { + + public UserNameInvalidException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java index 7959b3655..22549af1d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java @@ -1,22 +1,25 @@ package com.sprint.mission.discodeit.domain.user.validation; +import com.sprint.mission.discodeit.domain.user.exception.EmailInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; import java.util.regex.Pattern; -import org.springframework.util.Assert; public class EmailValidator { - private final static String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\." + "[a-zA-Z0-9_+&*-]+)*@" + "(?:[a-zA-Z0-9-]+\\.)+[a-z" + "A-Z]{2,7}$"; + private final static String EMAIL_REGEX = + "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z]{2,7})+$"; + private final static Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); public static void valid(String email) { - Assert.notNull(email, "이메일은 필수입니다."); - if (email.isBlank()) { - throw new IllegalArgumentException("이메일은 필수입니다."); + if (Objects.isNull(email)) { + throw new EmailInvalidException(ErrorCode.EMAIL_REQUIRED, ""); } - - if (!EMAIL_PATTERN.matcher(email).matches()) { - throw new IllegalArgumentException("이메일 형식이 틀렸습니다 : 입력 = " + email); + if (email.isBlank() || !EMAIL_PATTERN.matcher(email).matches()) { + throw new EmailInvalidException(ErrorCode.INVALID_EMAIL_FORMAT, email); } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java index 96afad98a..c741fc717 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java @@ -1,23 +1,31 @@ package com.sprint.mission.discodeit.domain.user.validation; -import org.springframework.util.Assert; +import com.sprint.mission.discodeit.domain.user.exception.NickNameInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; +import java.util.Set; public class NicknameValidator { - private static final int MAX_LENGTH = 15; - private static final int MIN_LENGTH = 1; - private static final String LENGTH_ERROR_MESSAGE = "닉네임은 " + MIN_LENGTH + " ~ " + MAX_LENGTH + "제한입니다. : 입력한 이름 길이 = "; + private static final int MAX_LENGTH = 32; + private static final Set FORBIDDEN_WORDS = Set.of( + "admin", "moderator", "discord", "system", "root", "bot", "mod", + "운영자", "관리자", "봇" + ); public static void validate(String value) { - Assert.notNull(value, "닉네임은 필수입니다."); + if (Objects.isNull(value)) { + throw new NickNameInvalidException(ErrorCode.NICKNAME_REQUIRED, ""); + } - if (value.isBlank() || value.length() > MAX_LENGTH || value.length() < MIN_LENGTH) { - throw new IllegalArgumentException(provideLengthNameErrorMessage(value)); + if (value.isBlank() || value.length() > MAX_LENGTH) { + throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_LENGTH, value); } - } - // TODO 에러 관리 따로 관리하기. - private static String provideLengthNameErrorMessage(String value) { - return LENGTH_ERROR_MESSAGE + value.length(); + String lowerCase = value.toLowerCase(); + for (String word : FORBIDDEN_WORDS) { + if (lowerCase.contains(word)) { + throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_FORMAT, value); + } + } } - } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java new file mode 100644 index 000000000..e373bffdd --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java @@ -0,0 +1,42 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + +public class UsernameValidator { + private static final int MIN_LENGTH = 2; + private static final int MAX_LENGTH = 32; + private static final String VALID_USER_NAME_REGEX = + "^(?!.*\\.\\.)(?![.])(?!.*[.]$)[a-zA-Z0-9_.]{2,32}$"; + private static final Pattern VALID_USER_NAME_PATTERN = Pattern.compile(VALID_USER_NAME_REGEX); + private static final Set FORBIDDEN_WORD = + Set.of( + "admin", "moderator", "discord", "system", "root", "bot", "mod", + "운영자", "관리자", "봇" + ); + + + public static void validate(final String username) { + if (Objects.isNull(username)) { + throw new UserNameInvalidException(ErrorCode.USERNAME_REQUIRED, ""); + } + + if (username.length() > MAX_LENGTH || username.length() < MIN_LENGTH) { + throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_LENGTH, username); + } + + if (!VALID_USER_NAME_PATTERN.matcher(username).matches()) { + throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); + } + + String lowerUsername = username.toLowerCase(); + for (String word : FORBIDDEN_WORD) { + if (lowerUsername.contains(word)) { + throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); + } + } + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java deleted file mode 100644 index 9ea3a147a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/binarycontent/BinaryContent.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.entity.binarycontent; - -import java.io.File; -import java.time.Instant; -import java.util.Objects; -import java.util.UUID; -import lombok.Getter; - -@Getter -public class BinaryContent { - private final UUID id; - private final Instant createdAt; - private final File file; - - public BinaryContent(File file) { - Objects.requireNonNull(file, "file is null"); - this.id = UUID.randomUUID(); - this.createdAt = Instant.now(); - this.file = file; - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java deleted file mode 100644 index 3db158c01..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/Channel.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.sprint.mission.discodeit.entity.channel; - -import com.google.common.base.Preconditions; -import com.sprint.mission.discodeit.common.error.ErrorMessage; -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.entity.common.BaseEntity; -import com.sprint.mission.discodeit.entity.user.entity.User; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Getter; - -@Getter -public class Channel extends BaseEntity { - - @NotNull - @Size( - min = 3, max = 50, - message = "create channel must be between {min} and {max} : reject channel name `${validatedValue}`" - ) - private String channelName; - - private final User creator; - - private Channel(String channelName, User creator) { - this.channelName = channelName; - this.creator = creator; - } - - public static Channel createOfChannelNameAndUser( - String channelName, - User creator - ) { - - return new Channel(channelName, creator); - } - - public static Channel createDefaultNameAndUser(User user) { - - return new Channel("Default Channel Name", user); - } - - public void changeName(String newName, User user) { - checkCreatorEqualsOrThrow(user); - - channelName = newName; - updateStatusAndUpdateAt(); - } - - public void deleteChannel(User user) { - checkCreatorEqualsOrThrow(user); - - updateUnregistered(); - } - - public boolean isStatusNotUnregisteredAndEqualsTo(String channelName) { - return isNotUnregistered() && this.channelName.equals(channelName); - } - - private void checkCreatorEqualsOrThrow(User user) { - var isNotCreator = isNotCreator(user); - - if (isNotCreator) { - throw ChannelException.ofErrorMessageAndCreatorName( - ErrorMessage.CHANNEL_NOT_EQUAL_CREATOR, - user.getNicknameValue() - ); - } - } - - private boolean isNotCreator(User user) { - Preconditions.checkNotNull(user); - return !creator.equals(user); - } - - public String getCreatorName() { - return creator.getNicknameValue(); - } - - @Override - public String toString() { - var format = - String.format( - "channel info = [channel name = %s creator = %s, createAt = %d, updateAt = %d, status = %s]", - channelName, - creator.getName(), - getCreateAt().toEpochMilli(), - getUpdateAt().toEpochMilli(), - getStatus().toString() - ); - return format; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChangeChannelNameRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChangeChannelNameRequest.java deleted file mode 100644 index 353426161..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChangeChannelNameRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.channel.dto; - -import java.util.UUID; - -public record ChangeChannelNameRequest(UUID userId, UUID channelId, String newChannelName) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChannelResponse.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChannelResponse.java deleted file mode 100644 index 5a115af80..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/ChannelResponse.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.sprint.mission.discodeit.entity.channel.dto; - -import java.util.UUID; - -public record ChannelResponse(UUID channelId, String channelName, String creator, String status) { - -// public static ChannelResponse ofChannelIdNameCreatorStatus(UUID channelId, String channelName, String creator, String status) { -// // 이게 맞나... 모든 파라미터를 null check -// Preconditions.checkNotNull(channelId); -// Preconditions.checkNotNull(channelName); -// Preconditions.checkNotNull(creator); -// Preconditions.checkNotNull(status); -// return new ChannelResponse(channelId, channelName, creator, status); -// } - - public static final class Builder { - private UUID channelId; - private String channelName; - private String creator; - private String status; - - public Builder() {} - - public Builder channelId(UUID channelId) { - this.channelId = channelId; - return this; - } - - public Builder channelName(String channelName) { - this.channelName = channelName; - return this; - } - - public Builder creator(String creator) { - this.creator = creator; - return this; - } - - public Builder status(String status) { - this.status = status; - return this; - } - - public ChannelResponse build() { - return new ChannelResponse(channelId, channelName, creator, status); - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/CreateNewChannelRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/CreateNewChannelRequest.java deleted file mode 100644 index ba483eacf..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/CreateNewChannelRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sprint.mission.discodeit.entity.channel.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.util.UUID; - -public record CreateNewChannelRequest( - @NotNull - UUID userId, - @NotBlank - String channelName -) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/DeleteChannelRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/DeleteChannelRequest.java deleted file mode 100644 index 59ad9711d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/channel/dto/DeleteChannelRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.channel.dto; - -import java.util.UUID; - -public record DeleteChannelRequest(UUID userId, UUID channelId) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java deleted file mode 100644 index f68cf9782..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/BaseEntity.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.sprint.mission.discodeit.entity.common; - -import static com.sprint.mission.discodeit.entity.common.Status.MODIFIED; -import static com.sprint.mission.discodeit.entity.common.Status.REGISTERED; -import static com.sprint.mission.discodeit.entity.common.Status.UNREGISTERED; - -import com.google.common.base.Preconditions; -import java.io.Serial; -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; -import lombok.Getter; - -@Getter -public abstract class BaseEntity implements Serializable { - - @Serial - private static final long serialVersionUID = -2898060967687082469L; - private final UUID id; - - private final Instant createAt; - - private Instant updateAt; - - private Status status; - - protected BaseEntity() { - this.id = UUID.randomUUID(); - this.createAt = createUnixTimestamp(); - this.updateAt = createUnixTimestamp(); - this.status = REGISTERED; - } - - private void updateStatus(Status status) { - Preconditions.checkNotNull(status); - this.status = status; - this.updateAt = createUnixTimestamp(); - } - - public void updateStatusAndUpdateAt() { - updateStatus(MODIFIED); - } - - public void updateUnregistered() { - updateStatus(UNREGISTERED); - } - - public boolean isNotUnregistered() { - return status != UNREGISTERED; - } - - private Instant createUnixTimestamp() { - return Instant.now(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - BaseEntity that = (BaseEntity) o; - return id.equals(that.id); - } - - @Override - public int hashCode() { - return id.hashCode(); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/Status.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/Status.java deleted file mode 100644 index a68e90b3d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/common/Status.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sprint.mission.discodeit.entity.common; - -public enum Status { - - MODIFIED("수정"), - REGISTERED("등록"), - UNREGISTERED("해지") - ; - - private final String status; - - Status(String status) { - this.status = status; - } - - public String getStatus() { - return status; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java deleted file mode 100644 index 0f252ebef..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/ChannelMessage.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sprint.mission.discodeit.entity.message; - -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.common.BaseEntity; -import com.sprint.mission.discodeit.entity.user.entity.User; -import lombok.Getter; - -@Getter -public class ChannelMessage extends BaseEntity { - - private final Sender sender; - - private final String message; - - private final User messageSender; - - private final Channel receiverChannel; - - private ChannelMessage(Sender sender, String message, User messageSender, Channel receiverChannel) { - this.sender = sender; - this.message = message; - this.messageSender = messageSender; - this.receiverChannel = receiverChannel; - } - - public static ChannelMessage ofMessageAndSenderAndReceiverChannel( - String message, - User messageSender, - Channel receiverChannel - ) { - - Sender channelSender = (sender, receiver, message1) -> { - var format = String.format( - "보낸 사람 : %s , 수신 채널 : %s , 메시지 : %s", - sender.getName(), - receiver.getChannelName(), - message1 - ); - - System.out.println(format); - }; - - return new ChannelMessage(channelSender, message, messageSender, receiverChannel); - } - - public void sendMessage() { - sender.sendMessage(messageSender, receiverChannel, message); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java deleted file mode 100644 index aa2404042..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/DirectMessage.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sprint.mission.discodeit.entity.message; - -import com.sprint.mission.discodeit.entity.common.BaseEntity; -import com.sprint.mission.discodeit.entity.user.entity.User; -import lombok.Getter; - -@Getter -public class DirectMessage extends BaseEntity { - // 보내는 방법을 추상화 - private final transient Sender sender; - - private final String message; - - private final User messageSender; - - private final User messageReceiver; - - private DirectMessage(Sender sender, String message, User messageSender, User messageReceiver) { - this.sender = sender; - this.message = message; - this.messageSender = messageSender; - this.messageReceiver = messageReceiver; - } - - public static DirectMessage ofMessageAndSenderReceiver(String message, User messageSender, User messageReceiver) { - Sender sender = (sender1, receiver1, message1) -> { - var format = String.format( - "보낸 사람 : %s , 보낸 사람 : %s, 메시지 : %s", - sender1.getName(), - receiver1.getName(), - message - ); - }; - - return new DirectMessage(sender, message, messageSender, messageReceiver); - } - - public void sendMessage() { - sender.sendMessage(messageSender, messageReceiver, message); - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/Sender.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/Sender.java deleted file mode 100644 index 0e045fe12..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/Sender.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.message; - -public interface Sender { - - void sendMessage(T sender, V receiver, String message); -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/ChannelMessageInfoResponse.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/ChannelMessageInfoResponse.java deleted file mode 100644 index b4e45760b..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/ChannelMessageInfoResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sprint.mission.discodeit.entity.message.dto; - -import java.util.UUID; - -public record ChannelMessageInfoResponse(UUID messageId, UUID sendUserId, UUID receiveChannelId, String message) { - - public static final class Builder { - private UUID messageId; - private UUID sendUserId; - private UUID receiveChannelId; - private String message; - - public Builder messageId(UUID messageId) { - this.messageId = messageId; - return this; - } - - public Builder sendUserId(UUID sendUserId) { - this.sendUserId = sendUserId; - return this; - } - - public Builder receiveChannelId(UUID receiveChannelId) { - this.receiveChannelId = receiveChannelId; - return this; - } - - public Builder message(String message) { - this.message = message; - return this; - } - - public ChannelMessageInfoResponse build() { - return new ChannelMessageInfoResponse(messageId, sendUserId, receiveChannelId, message); - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/DirectMessageInfoResponse.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/DirectMessageInfoResponse.java deleted file mode 100644 index 12b33ab8d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/DirectMessageInfoResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.sprint.mission.discodeit.entity.message.dto; - -import java.util.UUID; - -public record DirectMessageInfoResponse(UUID messageId, UUID sender, UUID receiver, String message) { - - public static final class Builder { - private UUID messageId; - private UUID sender; - private UUID receiver; - private String message; - - public Builder messageId(UUID messageId) { - this.messageId = messageId; - return this; - } - - public Builder sender(UUID sender) { - this.sender = sender; - return this; - } - - public Builder receiver(UUID receiver) { - this.receiver = receiver; - return this; - } - - public Builder message(String message) { - this.message = message; - return this; - } - - public DirectMessageInfoResponse build() { - return new DirectMessageInfoResponse(messageId, sender, receiver, message); - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendChannelMessageRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendChannelMessageRequest.java deleted file mode 100644 index af623b217..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendChannelMessageRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sprint.mission.discodeit.entity.message.dto; - -import java.util.UUID; - -public record SendChannelMessageRequest(UUID sendUserId, UUID receiveChannelId, String message) { - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendDirectMessageRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendDirectMessageRequest.java deleted file mode 100644 index 2d79975a4..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/message/dto/SendDirectMessageRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.message.dto; - -import java.util.UUID; - -public record SendDirectMessageRequest(UUID sendUserId, UUID receiveUserId, String message) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java deleted file mode 100644 index bf0a0ce23..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/readstatus/ReadStatus.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sprint.mission.discodeit.entity.readstatus; - -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.time.Instant; -import java.util.Objects; -import java.util.UUID; -import lombok.Getter; - -@Getter -public class ReadStatus { - - private final UUID id; - private final Instant createdAt; - private Instant updatedAt; - private boolean isRead; - private final User user; - - public ReadStatus(User user) { - Objects.requireNonNull(user, "user is null"); - this.id = UUID.randomUUID(); - this.createdAt = Instant.now(); - this.updatedAt = Instant.now(); - this.isRead = false; - this.user = user; - } - - public void updateReadStatusByReading() { - this.isRead = true; - this.updatedAt = Instant.now(); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ExitChannelRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ExitChannelRequest.java deleted file mode 100644 index be7b46381..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ExitChannelRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.dto; - -import java.util.UUID; - -public record ExitChannelRequest(UUID userId, UUID channelId) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/FindUserRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/FindUserRequest.java deleted file mode 100644 index 78dba1e3e..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/FindUserRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.dto; - -public record FindUserRequest(String username) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ModifyUserInfoRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ModifyUserInfoRequest.java deleted file mode 100644 index d414afc40..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/ModifyUserInfoRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.dto; - -import java.util.UUID; - -public record ModifyUserInfoRequest(UUID id, String changeUsername) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/RegisterUserRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/RegisterUserRequest.java deleted file mode 100644 index e910c87e4..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/RegisterUserRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.dto; - -public record RegisterUserRequest(String name) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UnregisterUserRequest.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UnregisterUserRequest.java deleted file mode 100644 index 502ea8ae6..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UnregisterUserRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.dto; - -import java.util.UUID; - -public record UnregisterUserRequest(UUID id, String username) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UserInfoResponse.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UserInfoResponse.java deleted file mode 100644 index 37f6a46d2..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/dto/UserInfoResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.dto; - -import com.sprint.mission.discodeit.entity.common.Status; -import java.util.UUID; - -public record UserInfoResponse(UUID uuid, String username, Status status) { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java deleted file mode 100644 index 12d051197..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannel.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.entity; - -import static com.sprint.mission.discodeit.common.error.ErrorMessage.USER_NOT_PARTICIPATED_CHANNEL; - -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.common.Status; -import java.io.Serial; -import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import lombok.Getter; - -public class ParticipatedChannel implements Serializable { - - @Serial - private static final long serialVersionUID = -2880824194029612128L; - - private final Map participatedChannels; - - private ParticipatedChannel(Map channels) { - this.participatedChannels = channels; - } - - public static ParticipatedChannel newDefault() { - return new ParticipatedChannel(new HashMap<>()); - } - - public Channel createChannel(String channelName, User user) { - var createdChannel = Channel.createOfChannelNameAndUser(channelName, user); - participatedChannels.put(createdChannel.getId(), createdChannel); - - return createdChannel; - } - - public List findAllChannels() { - var participatedChannels = - this.participatedChannels.values() - .stream() - .filter(Channel::isNotUnregistered) - .toList(); - - return Collections.unmodifiableList(participatedChannels); - } - - private Optional findByChannelId(UUID channelId) { - var foundParticipatedChannel = participatedChannels.get(channelId); - return Optional.ofNullable(foundParticipatedChannel); - } - - public Optional findByChannelIdNotUnregisteredOrThrow(UUID channelId) { - var foundChannel = - findByChannelId(channelId) - .orElseThrow( - () -> ChannelException.ofErrorMessageAndNotExistChannelId(USER_NOT_PARTICIPATED_CHANNEL, channelId)); - - var unregisteredReturnNullOrFoundChannel = - foundChannel.getStatus() == Status.UNREGISTERED ? null : foundChannel; - - return Optional.ofNullable(unregisteredReturnNullOrFoundChannel); - } - - // TODO 같은 이름을 가진 채널이 여러 개 있을 수 있음. 리스트로 반환 고려해야하지 않을까? - public Optional findByName(String name) { - var foundChannelByName = participatedChannels.values() - .stream() - .filter(channel -> channel.isStatusNotUnregisteredAndEqualsTo(name)) - .findFirst(); - - return foundChannelByName; - } - - public Channel changeChannelNameOrThrow(UUID channelId, String newName, User user) { - var foundChannel = - findByChannelIdNotUnregisteredOrThrow(channelId) - .orElseThrow( - () -> UserException.ofErrorMessageAndId(USER_NOT_PARTICIPATED_CHANNEL, - channelId.toString())); - - foundChannel.changeName(newName, user); - participatedChannels.put(foundChannel.getId(), foundChannel); - - return foundChannel; - } - - public void exitChannelById(UUID channelId) { - findByChannelId(channelId) - .ifPresent(foundChannel -> participatedChannels.remove(foundChannel.getId())); - } - - public int countParticipatedChannels() { - return participatedChannels.size(); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java deleted file mode 100644 index fe3a782fa..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/User.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.entity; - - -import com.google.common.base.Preconditions; -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.common.BaseEntity; -import com.sprint.mission.discodeit.domain.user.Email; -import com.sprint.mission.discodeit.domain.user.Nickname; -import java.util.List; -import java.util.UUID; -import lombok.Getter; -import lombok.ToString; - -@Getter -@ToString -public class User extends BaseEntity { - - private Nickname name; - private Email email; - private final ParticipatedChannel participatedChannels; - - private User(Nickname name, ParticipatedChannel channel) { - this.name = name; - this.participatedChannels = channel; - } - - public static User createFrom(String username) { - var userName = new Nickname(username); - var participatedChannel = ParticipatedChannel.newDefault(); - return new User(userName, participatedChannel); - } - - public void changeUserName(String newName) { - this.name = new Nickname(newName); - updateStatusAndUpdateAt(); - } - - public Channel openNewChannel(String channelName) { - Preconditions.checkNotNull(channelName); - var createdChannel = participatedChannels.createChannel(channelName, this); - return createdChannel; - } - - public Channel changeChannelName(UUID channelId, String channelName) { - Preconditions.checkNotNull(channelId); - var targetChannel = - participatedChannels.changeChannelNameOrThrow(channelId, channelName, this); - - return targetChannel; - } - - public void exitParticipatedChannel(UUID channelId) { - participatedChannels.exitChannelById(channelId); - } - - public int countParticipatedChannels() { - var participatedChannelCount = participatedChannels.countParticipatedChannels(); - return participatedChannelCount; - } - - public List getParticipatedChannels() { - return participatedChannels.findAllChannels(); - } - - public void unregister() { - updateUnregistered(); - } - - public String getNicknameValue() { - return name.getValue(); - } - -} - diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java deleted file mode 100644 index 9c3e7282a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/user/entity/UserName.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.entity; - - -import com.google.common.base.Preconditions; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.io.Serial; -import java.io.Serializable; -import java.util.Objects; -import lombok.Getter; - -@Getter -public class UserName implements Serializable { - - public static final int NAME_MIN_LENGTH = 3; - public static final int NAME_MAX_LENGTH = 20; - @Serial - private static final long serialVersionUID = 8759111145208252347L; - - @Size( - min = NAME_MIN_LENGTH, max = NAME_MAX_LENGTH, - message = "'${validatedValue}' must be between {min} and {max} characters long" - ) - @NotNull - private final String name; - - public UserName(String name) { - this.name = name; - } - - public static UserName createFrom(String username) { - Preconditions.checkNotNull(username); - return new UserName(username); - } - - public UserName changeName(String name) { - Preconditions.checkNotNull(name); - return new UserName(name); - } - - @Override - public String toString() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - UserName userName = (UserName) o; - return Objects.equals(name, userName.name); - } - - - @Override - public int hashCode() { - return Objects.hashCode(name); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java deleted file mode 100644 index c887a4689..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/entity/userstatus/UserStatus.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sprint.mission.discodeit.entity.userstatus; - -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Objects; -import java.util.UUID; -import lombok.Getter; - -@Getter -public class UserStatus { - - private final UUID id; - private Instant createdAt; - private Instant updatedAt; - private final User user; - - public UserStatus(User user) { - Objects.requireNonNull(user, "user is null"); - this.id = UUID.randomUUID(); - this.user = user; - this.createdAt = Instant.now(); - this.updatedAt = Instant.now(); - } - public void updateConnectTime() { - updatedAt = Instant.now(); - } - public boolean isActiveWithInFiveMinutes() { - Instant withInFiveMin = Instant.now().minus(5, ChronoUnit.MINUTES); - return updatedAt.isAfter(withInFiveMin); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java new file mode 100644 index 000000000..250ab4e6a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -0,0 +1,48 @@ +package com.sprint.mission.discodeit.global.error; + +public enum ErrorCode { + + + // User Domain + INVALID_USERNAME_LENGTH(400, "유저 이름은 1~32자 이내여야 합니다.", "U001"), + INVALID_NICKNAME_LENGTH(400, "유저 이름은 2~32자 이내여야 합니다.", "U002"), + INVALID_PASSWORD_LENGTH(400, "비밀번호는 8~20자 이내여야 합니다.", "U004"), // 비밀번호 길이 오류 + WEAK_PASSWORD(400, "비밀번호는 대문자, 숫자, 특수문자를 포함해야 합니다.", "U008"), + + USERNAME_REQUIRED(400, "유저 이름은 필수입니다.", "U104"), + NICKNAME_REQUIRED(400, "닉네임은 필수입니다.", "U105"), + EMAIL_REQUIRED(400, "이메일은 필수입니다.", "U106"), + PASSWORD_REQUIRED(400, "비밀번호는 필수 입력값입니다.", "U107"), + + INVALID_EMAIL_FORMAT(400, "이메일 형식이 올바르지 않습니다", "U203"), + INVALID_USERNAME_FORMAT(400, "유저 이름에 허용되지 않은 문자가 포함되어 있습니다.", "U205"), + INVALID_NICKNAME_FORMAT(400, "닉네임에 허용되지 않은 문자가 포함되어 있습니다.", "U206"), + + DUPLICATE_EMAIL(400, "이미 사용중인 이메일입니다.", "U307"), + DUPLICATE_USERNAME(400, "이미 존재하는 유저이름입니다.", "U308"), + + + ; + + private final int status; + private final String description; + private final String code; + + ErrorCode(int status, String description, String code) { + this.status = status; + this.description = description; + this.code = code; + } + + public int getStatus() { + return status; + } + + public String getDescription() { + return description; + } + + public String getCode() { + return code; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java new file mode 100644 index 000000000..cfe26b86e --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.global.error.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + + protected final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getDescription()); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode, String message) { + super(errorCode.getDescription().concat(" 입력값 = ").concat(message)); + this.errorCode = errorCode; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/EntityNotFoundException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/EntityNotFoundException.java new file mode 100644 index 000000000..ec5c9bd20 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/EntityNotFoundException.java @@ -0,0 +1,10 @@ +package com.sprint.mission.discodeit.global.error.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; + +public class EntityNotFoundException extends BusinessException { + + public EntityNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/InvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/InvalidException.java new file mode 100644 index 000000000..7ffa192a1 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/InvalidException.java @@ -0,0 +1,10 @@ +package com.sprint.mission.discodeit.global.error.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; + +public class InvalidException extends BusinessException { + + public InvalidException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/CrudRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/CrudRepository.java deleted file mode 100644 index 0ac8f04f9..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/CrudRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sprint.mission.discodeit.repository.common; - -import java.io.FileNotFoundException; -import java.util.Optional; - -public interface CrudRepository extends Repository { - - T save(T entity); - - Optional findById(ID id); - - Iterable findAll(); - - int count(); - - void deleteById(ID id); - - boolean isExistsById(ID id); -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java deleted file mode 100644 index ee9058ab0..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepository.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.sprint.mission.discodeit.repository.common; - -import com.sprint.mission.discodeit.entity.common.BaseEntity; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; - -public abstract class InMemoryCrudRepository - implements CrudRepository { - - protected final Map store = new HashMap<>(); - - @Override - public final T save(T entity) { - var id = Objects.requireNonNull(entity.getId()); - store.put(id, entity); - return entity; - } - - @Override - public Optional findById(ID id) { - var findEntity = store.get(id); - return Optional.ofNullable(findEntity); - } - - @Override - public List findAll() { - if (store.isEmpty()) { - return Collections.emptyList(); - } - - var existEntities = store.values() - .stream() - .toList(); - - return Collections.unmodifiableList(existEntities); - } - - @Override - public int count() { - return store.size(); - } - - @Override - public void deleteById(ID id) { - store.remove(id); - } - - @Override - public boolean isExistsById(ID id) { - var isExist = store.containsKey(id); - return isExist; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/Repository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/Repository.java deleted file mode 100644 index dc48a9ae8..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/common/Repository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.sprint.mission.discodeit.repository.common; - -public interface Repository { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java deleted file mode 100644 index 7de412af8..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/FileAbstractRepository.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.sprint.mission.discodeit.repository.file; - -import com.sprint.mission.discodeit.entity.common.BaseEntity; -import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public abstract class FileAbstractRepository - extends InMemoryCrudRepository { - - private final File file; - - protected FileAbstractRepository(String filePath) { - file = new File(filePath); - } - - protected Map loadFile() { - if (!file.exists()) { - return Collections.emptyMap(); - } - - Map store = new HashMap<>(); - try ( - FileInputStream fis = new FileInputStream(file); - ObjectInputStream ois = new ObjectInputStream(fis); - ) { - try { - store = (Map) ois.readObject(); - } catch (ClassNotFoundException e) { - System.out.println(e.getMessage()); - } - } catch (IOException e) { - System.out.println("loading exit"); - throw new IllegalArgumentException(); - } - return store; - } - - - public void saveToFile() { - try ( - FileOutputStream fos = new FileOutputStream(file); - ObjectOutputStream oos = new ObjectOutputStream(fos); - ) { - oos.writeObject(store); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - -} - diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java deleted file mode 100644 index 5995be58a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/channel/FileChannelRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sprint.mission.discodeit.repository.file.channel; - -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.file.FileAbstractRepository; -import java.util.UUID; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Repository; - -@Repository -public class FileChannelRepository extends FileAbstractRepository implements ChannelRepository { - private static final String CHANNEL_FILE_PATH_NAME = "temp/file/channel/channel.ser"; - private static ChannelRepository FILE_USER_REPOSITORY_INSTANCE; - - protected FileChannelRepository() { - super(CHANNEL_FILE_PATH_NAME); - store.putAll(loadFile()); - } - - public static ChannelRepository getInstance() { - if (FILE_USER_REPOSITORY_INSTANCE == null) { - FILE_USER_REPOSITORY_INSTANCE = new FileChannelRepository(); - } - return FILE_USER_REPOSITORY_INSTANCE; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java deleted file mode 100644 index 55ca813c2..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileChannelMessageRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sprint.mission.discodeit.repository.file.message; - -import com.sprint.mission.discodeit.entity.message.ChannelMessage; -import com.sprint.mission.discodeit.repository.file.FileAbstractRepository; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; -import java.util.UUID; -import org.springframework.stereotype.Repository; - -@Repository -public class FileChannelMessageRepository extends FileAbstractRepository - implements ChannelMessageRepository { - - private static final String CHANNEL_MESSAGE_FILE_PATH_NAME = "temp/file/user/channelMessage.ser"; - private static ChannelMessageRepository INSTANCE; - - - protected FileChannelMessageRepository() { - super(CHANNEL_MESSAGE_FILE_PATH_NAME); - store.putAll(loadFile()); - } - - public static ChannelMessageRepository getInstance() { - if (INSTANCE == null) { - INSTANCE = new FileChannelMessageRepository(); - } - return INSTANCE; - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileDirectMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileDirectMessageRepository.java deleted file mode 100644 index 68db4315a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/message/FileDirectMessageRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sprint.mission.discodeit.repository.file.message; - -import com.sprint.mission.discodeit.entity.message.DirectMessage; -import com.sprint.mission.discodeit.repository.file.FileAbstractRepository; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import java.util.UUID; -import org.springframework.stereotype.Repository; - -@Repository -public class FileDirectMessageRepository extends FileAbstractRepository implements - DirectMessageRepository { - private static final String DIRECT_MESSAGE_FILE_PATH_NAME = "temp/file/message/directmessage.ser"; - private static FileDirectMessageRepository FILE_DIRECT_REPOSITORY_INSTANCE; - - protected FileDirectMessageRepository() { - super(DIRECT_MESSAGE_FILE_PATH_NAME); - store.putAll(loadFile()); - } - - public static DirectMessageRepository getInstance() { - if (FILE_DIRECT_REPOSITORY_INSTANCE == null) { - FILE_DIRECT_REPOSITORY_INSTANCE = new FileDirectMessageRepository(); - } - return FILE_DIRECT_REPOSITORY_INSTANCE; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java deleted file mode 100644 index 3d7677e13..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/file/user/FileUserRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sprint.mission.discodeit.repository.file.user; - -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.repository.file.FileAbstractRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import java.util.Optional; -import java.util.UUID; -import org.springframework.stereotype.Repository; - -@Repository -public class FileUserRepository extends FileAbstractRepository implements UserRepository { - private static final String FILE_PATH_USER_NAME = "temp/file/user/user.ser"; - private static UserRepository FILE_USER_REPOSITORY_INSTANCE; - - private FileUserRepository() { - super(FILE_PATH_USER_NAME); - store.putAll(loadFile()); - } - - public static UserRepository getInstance() { - if (FILE_USER_REPOSITORY_INSTANCE == null) { - FILE_USER_REPOSITORY_INSTANCE = new FileUserRepository(); - } - return FILE_USER_REPOSITORY_INSTANCE; - } - - @Override - public Optional findByUsername(String username) { - var findUser = findAll().stream() - .filter(user -> username.equals(user.getName())) - .findFirst(); - - return findUser; - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/ChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/ChannelRepository.java deleted file mode 100644 index 1a5bbc5fc..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/ChannelRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.channel; - -import com.sprint.mission.discodeit.repository.common.CrudRepository; -import com.sprint.mission.discodeit.entity.channel.Channel; -import java.util.UUID; - -public interface ChannelRepository extends CrudRepository { - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/JCFChannelRepositoryInMemory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/JCFChannelRepositoryInMemory.java deleted file mode 100644 index 68faf6f8e..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/channel/JCFChannelRepositoryInMemory.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.channel; - -import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import com.sprint.mission.discodeit.entity.channel.Channel; -import java.util.UUID; - -public class JCFChannelRepositoryInMemory extends InMemoryCrudRepository implements ChannelRepository { - private JCFChannelRepositoryInMemory() {} - - public static ChannelRepository getChannelRepositoryInMemory() { - return new JCFChannelRepositoryInMemory(); - } - - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/ChannelMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/ChannelMessageRepository.java deleted file mode 100644 index b78ce6471..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/ChannelMessageRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage; - -import com.sprint.mission.discodeit.repository.common.CrudRepository; -import com.sprint.mission.discodeit.entity.message.ChannelMessage; -import java.util.UUID; - -public interface ChannelMessageRepository extends CrudRepository { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/JCFChannelMessageRepositoryInMemory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/JCFChannelMessageRepositoryInMemory.java deleted file mode 100644 index e6559940a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/ChannelMessage/JCFChannelMessageRepositoryInMemory.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage; - -import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import com.sprint.mission.discodeit.entity.message.ChannelMessage; -import java.util.UUID; - -public class JCFChannelMessageRepositoryInMemory extends InMemoryCrudRepository implements ChannelMessageRepository { - - private JCFChannelMessageRepositoryInMemory() {} - - public static JCFChannelMessageRepositoryInMemory getInstance() { - return new JCFChannelMessageRepositoryInMemory(); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/DirectMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/DirectMessageRepository.java deleted file mode 100644 index 9cbd28670..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/DirectMessageRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.message.directMessage; - -import com.sprint.mission.discodeit.repository.common.CrudRepository; -import com.sprint.mission.discodeit.entity.message.DirectMessage; -import java.util.UUID; - -public interface DirectMessageRepository extends CrudRepository { - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/JCFDirectMessageRepositoryInMemory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/JCFDirectMessageRepositoryInMemory.java deleted file mode 100644 index 6144a3735..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/message/directMessage/JCFDirectMessageRepositoryInMemory.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.message.directMessage; - -import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import com.sprint.mission.discodeit.entity.message.DirectMessage; -import java.util.UUID; - -public class JCFDirectMessageRepositoryInMemory extends InMemoryCrudRepository - implements DirectMessageRepository { - - private JCFDirectMessageRepositoryInMemory() {} - - public static JCFDirectMessageRepositoryInMemory getInstance() { - return new JCFDirectMessageRepositoryInMemory(); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java deleted file mode 100644 index d64751eac..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/JCFUserRepositoryInMemory.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.user; - -import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.util.Optional; -import java.util.UUID; - -public class JCFUserRepositoryInMemory extends InMemoryCrudRepository implements UserRepository { - - private static UserRepository INSTANCE; - - private JCFUserRepositoryInMemory() {} - - @Override - public Optional findByUsername(String username) { - var findUser = findAll().stream() - .filter(user -> username.equals(user.getNicknameValue())) - .findFirst(); - - return findUser; - } - - - public static UserRepository getInstance() { - if (INSTANCE == null) { - INSTANCE = new JCFUserRepositoryInMemory(); - } - - return INSTANCE; - } - - public static UserRepository getUserRepositoryInMemory() { - return new JCFUserRepositoryInMemory(); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/UserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/UserRepository.java deleted file mode 100644 index 4f22cf196..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/jcf/user/UserRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sprint.mission.discodeit.repository.jcf.user; - -import com.sprint.mission.discodeit.repository.common.CrudRepository; -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.util.Optional; -import java.util.UUID; - -public interface UserRepository extends CrudRepository { - - Optional findByUsername(String username); - - public static UserRepository getInMemoryUserRepositoryImpl() { - return JCFUserRepositoryInMemory.getInstance(); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java deleted file mode 100644 index 276a0c165..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelMessageService.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.sprint.mission.discodeit.service.basic; - -import com.sprint.mission.discodeit.common.error.ErrorMessage; -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.message.ChannelMessage; -import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendChannelMessageRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.message.channelMessage.ChannelMessageService; -import com.sprint.mission.discodeit.service.message.converter.ChannelMessageConverter; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class BasicChannelMessageService implements ChannelMessageService { - - private final UserRepository userRepository; - private final ChannelRepository channelRepository; - private final ChannelMessageRepository channelMessageRepository; - private final ChannelMessageConverter converter; - - public static ChannelMessageService getInstance( - UserRepository userRepository, - ChannelRepository channelRepository, - ChannelMessageRepository channelMessageRepository - ) { - var converter = new ChannelMessageConverter(); - return new BasicChannelMessageService(userRepository, channelRepository, channelMessageRepository, converter); - } - - @Override - public ChannelMessageInfoResponse sendMessage(SendChannelMessageRequest sendChannelMessageRequest) { - // 유저 찾기 - var foundUser = findUserByIdOrThrow(sendChannelMessageRequest.sendUserId()); - // 채널 찾기 - var foundChannel = findChannelByIdOrThrow(sendChannelMessageRequest.receiveChannelId()); - // 메시지 생성 - var channelMessage = ChannelMessage.ofMessageAndSenderAndReceiverChannel( - sendChannelMessageRequest.message(), - foundUser, - foundChannel - ); - // 메시지 저장 - var savedMessage = channelMessageRepository.save(channelMessage); - // 메시지 전송 - savedMessage.sendMessage(); - // 생성된 메시지 반환 - return converter.toDto(savedMessage); - } - - /** - * ==> 코드리뷰 부탁드립니다. 유저 서비스에 다른 서비스에서 이용하기 위한 메서드를 넣는게 바람직한가요? - * ==> 제 생각은, 유저 서비스에 같은 기능으로 호출하는 기능이 필요하다면 추가하지만, 없다면 안넣는게 낫다고 생각함. 넣는다면 userRepository - * 그런데, 유저 서비스에서 id로만 찾는 유저를 찾는 기능이 있는가? 구현한 내용은 유저 서비스 레이어 안에서만 private 구현 - * 코드 중복이 너무 많이 발생함, 이를 해결하기 위해 userService, userRepository 둘 중 하나에 메서드를 만드는 방법 중 어디가 좋을까요? - * - * ==> 다른 서비스 레이어를 의존하도록 해서 호출해도 괜찮을까요 ? - */ - private User findUserByIdOrThrow(UUID userId) { - var foundUser = userRepository.findById(userId) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.ofErrorMessageAndId( - ErrorMessage.USER_NOT_FOUND, userId.toString() - )); - - return foundUser; - } - - - private Channel findChannelByIdOrThrow(UUID channelId) { - var foundChannel = channelRepository.findById(channelId) - .orElseThrow(() -> ChannelException.ofErrorMessageAndNotExistChannelId( - ErrorMessage.CHANNEL_NOT_FOUND, - channelId - )); - return foundChannel; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java deleted file mode 100644 index 80095c8bd..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.sprint.mission.discodeit.service.basic; - -import static com.sprint.mission.discodeit.common.error.ErrorMessage.CHANNEL_NOT_FOUND; -import static com.sprint.mission.discodeit.common.error.ErrorMessage.USER_NOT_FOUND; - -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.entity.channel.dto.ChangeChannelNameRequest; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; -import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; -import com.sprint.mission.discodeit.entity.channel.dto.DeleteChannelRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.channel.ChannelConverter; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class BasicChannelService implements ChannelService { - - private final ChannelRepository channelRepository; - private final UserRepository userRepository; - private final ChannelConverter channelConverter; - - public static ChannelService getInstance(UserRepository userRepository, ChannelRepository channelRepository) { - return new BasicChannelService(channelRepository, userRepository, new ChannelConverter()); - } - - @Override - public ChannelResponse createChannelOrThrow(CreateNewChannelRequest request) { - /** - * ==> 코드리뷰 : 메서드 내부에 다른 메서드를 호출하면서 throw가 발생가능하다는 것을 코드 블록의 메서드의 이름에 추가해야할까요? - * createChannel VS createChannelOrThrow - */ - var findUser = findUserByIdOrThrow(request.userId()); - - var createdChannel = findUser.openNewChannel(request.channelName()); - - channelRepository.save(createdChannel); - userRepository.save(findUser); - - return channelConverter.toDto(createdChannel); - } - - @Override - public void changeChannelNameOrThrow(ChangeChannelNameRequest request) { - var foundUser = findUserByIdOrThrow(request.userId()); - - var changedChannel = foundUser.changeChannelName(request.channelId(), request.newChannelName()); - - channelRepository.save(changedChannel); - userRepository.save(foundUser); - } - - @Override - public void deleteChannelByChannelIdOrThrow(DeleteChannelRequest request) { - var foundChannel = channelRepository.findById(request.channelId()) - .orElseThrow( - () -> ChannelException.ofErrorMessageAndNotExistChannelId( - CHANNEL_NOT_FOUND, - request.channelId()) - ); - - var foundUser = findUserByIdOrThrow(request.userId()); - foundChannel.deleteChannel(foundUser); - - channelRepository.save(foundChannel); - } - - private User findUserByIdOrThrow(UUID id) { - var foundUser = userRepository.findById(id) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.of(USER_NOT_FOUND)); - - return foundUser; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java deleted file mode 100644 index 4d1507865..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicDirectMessageService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.sprint.mission.discodeit.service.basic; - -import com.sprint.mission.discodeit.common.error.ErrorMessage; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.entity.message.DirectMessage; -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendDirectMessageRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.message.converter.DirectMessageConverter; -import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class BasicDirectMessageService implements DirectMessageService { - - private final DirectMessageRepository directMessageRepository; - private final UserRepository userRepository; - private final DirectMessageConverter directMessageConverter; - - public static DirectMessageService getInstance( - DirectMessageRepository directMessageRepository, - UserRepository userRepository - ) { - var converter = new DirectMessageConverter(); - return new BasicDirectMessageService(directMessageRepository, userRepository, converter); - } - - @Override - public DirectMessageInfoResponse sendMessage(SendDirectMessageRequest sendDirectMessageRequest) { - - var foundSendUser = findUserByIdOrThrow(sendDirectMessageRequest.sendUserId()); - var foundReceiverUser = findUserByIdOrThrow(sendDirectMessageRequest.receiveUserId()); - - var createdDirectMessage = DirectMessage.ofMessageAndSenderReceiver( - sendDirectMessageRequest.message(), - foundSendUser, - foundReceiverUser - ); - - var savedDirectMessage = directMessageRepository.save(createdDirectMessage); - - createdDirectMessage.sendMessage(); - - return directMessageConverter.toDto(savedDirectMessage); - } - - private User findUserByIdOrThrow(UUID userId) { - var foundUser = userRepository.findById(userId) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.ofErrorMessageAndId( - ErrorMessage.USER_NOT_FOUND, userId.toString() - )); - - return foundUser; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java deleted file mode 100644 index c4370364b..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.sprint.mission.discodeit.service.basic; - -import static com.sprint.mission.discodeit.common.error.ErrorMessage.USER_NOT_FOUND; - -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.entity.user.dto.ExitChannelRequest; -import com.sprint.mission.discodeit.entity.user.dto.FindUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.ModifyUserInfoRequest; -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UnregisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.service.user.UserConverter; -import com.sprint.mission.discodeit.service.user.UserService; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class BasicUserService implements UserService { - - private final UserRepository userRepository; - private final UserConverter converter; - - @Override - public UserInfoResponse register(RegisterUserRequest registerUserRequest) { - var entity = converter.toEntity(registerUserRequest); - var savedEntity = userRepository.save(entity); - - return converter.toDto(savedEntity); - } - - @Override - public UserInfoResponse findUserByUsernameOrThrow(FindUserRequest findUserRequest) { - var foundUser = userRepository.findByUsername(findUserRequest.username()) - .filter(User::isNotUnregistered) - .map(converter::toDto) - .orElseThrow(() -> UserException.of(USER_NOT_FOUND)); - - return foundUser; - } - - @Override - public UserInfoResponse modifyUserInfo(ModifyUserInfoRequest request) { - var foundUser = findByUserIdOrThrow(request.id()); - - foundUser.changeUserName(request.changeUsername()); - userRepository.save(foundUser); - - var response = converter.toDto(foundUser); - return response; - } - - @Override - public void UnRegisterUser(UnregisterUserRequest request) { - var foundUser = findByUserIdOrThrow(request.id()); - - foundUser.unregister(); - - userRepository.save(foundUser); - } - - @Override - public void exitChannel(ExitChannelRequest request) { - var foundUser = findByUserIdOrThrow(request.userId()); - foundUser.exitParticipatedChannel(request.channelId()); - userRepository.save(foundUser); - } - - private User findByUserIdOrThrow(UUID id) { - var entity = userRepository.findById(id) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.of(USER_NOT_FOUND)); - return entity; - } - - public static BasicUserService getInstance(UserRepository userRepository) { - return new BasicUserService(userRepository, new UserConverter()); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java deleted file mode 100644 index 31a0307d1..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelConverter.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sprint.mission.discodeit.service.channel; - -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse.Builder; -import org.springframework.stereotype.Component; - -@Component -public class ChannelConverter { - - public ChannelResponse toDto(Channel channel) { - var channelResponse = new Builder() - .channelId(channel.getId()) - .channelName(channel.getChannelName()) - .creator(channel.getCreatorName()) - .status(channel.getStatus().getStatus()) - .build(); - - return channelResponse; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelService.java deleted file mode 100644 index 5a79aa1c6..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/channel/ChannelService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.sprint.mission.discodeit.service.channel; - -import com.sprint.mission.discodeit.entity.channel.dto.ChangeChannelNameRequest; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; -import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; -import com.sprint.mission.discodeit.entity.channel.dto.DeleteChannelRequest; - -public interface ChannelService { - ChannelResponse createChannelOrThrow(CreateNewChannelRequest request); - - void changeChannelNameOrThrow(ChangeChannelNameRequest request); - - void deleteChannelByChannelIdOrThrow(DeleteChannelRequest request); -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelMessageService.java deleted file mode 100644 index 684f6cd29..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelMessageService.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import com.sprint.mission.discodeit.common.error.ErrorMessage; -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.message.ChannelMessage.ChannelMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.message.ChannelMessage; -import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendChannelMessageRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.service.message.channelMessage.ChannelMessageService; -import com.sprint.mission.discodeit.service.message.converter.ChannelMessageConverter; -import java.util.UUID; - -public class JCFChannelMessageService implements ChannelMessageService { - - private final UserRepository userRepository; - private final ChannelRepository channelRepository; - private final ChannelMessageRepository channelMessageRepository; - private final ChannelMessageConverter converter; - - private JCFChannelMessageService( - UserRepository userRepository, - ChannelRepository channelRepository, - ChannelMessageRepository channelMessageRepository, - ChannelMessageConverter converter - ) { - this.userRepository = userRepository; - this.channelRepository = channelRepository; - this.channelMessageRepository = channelMessageRepository; - this.converter = converter; - } - - public static ChannelMessageService getInstance( - UserRepository userRepository, - ChannelRepository channelRepository, - ChannelMessageRepository channelMessageRepository - ) { - var converter = new ChannelMessageConverter(); - return new JCFChannelMessageService(userRepository, channelRepository, channelMessageRepository, converter); - } - - @Override - public ChannelMessageInfoResponse sendMessage(SendChannelMessageRequest sendChannelMessageRequest) { - // 유저 찾기 - var foundUser = findUserByIdOrThrow(sendChannelMessageRequest.sendUserId()); - // 채널 찾기 - var foundChannel = findChannelByIdOrThrow(sendChannelMessageRequest.receiveChannelId()); - // 메시지 생성 - var channelMessage = ChannelMessage.ofMessageAndSenderAndReceiverChannel( - sendChannelMessageRequest.message(), - foundUser, - foundChannel - ); - // 메시지 저장 - var savedMessage = channelMessageRepository.save(channelMessage); - // 메시지 전송 - savedMessage.sendMessage(); - // 생성된 메시지 반환 - return converter.toDto(savedMessage); - } - - /** - * ==> 코드리뷰 부탁드립니다. 유저 서비스에 다른 서비스에서 이용하기 위한 메서드를 넣는게 바람직한가요? - * ==> 제 생각은, 유저 서비스에 같은 기능으로 호출하는 기능이 필요하다면 추가하지만, 없다면 안넣는게 낫다고 생각함. 넣는다면 userRepository - * 그런데, 유저 서비스에서 id로만 찾는 유저를 찾는 기능이 있는가? 구현한 내용은 유저 서비스 레이어 안에서만 private 구현 - * 코드 중복이 너무 많이 발생함, 이를 해결하기 위해 userService, userRepository 둘 중 하나에 메서드를 만드는 방법 중 어디가 좋을까요? - * - * ==> 다른 서비스 레이어를 의존하도록 해서 호출해도 괜찮을까요 ? - */ - private User findUserByIdOrThrow(UUID userId) { - var foundUser = userRepository.findById(userId) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.ofErrorMessageAndId( - ErrorMessage.USER_NOT_FOUND, userId.toString() - )); - - return foundUser; - } - - - private Channel findChannelByIdOrThrow(UUID channelId) { - var foundChannel = channelRepository.findById(channelId) - .orElseThrow(() -> ChannelException.ofErrorMessageAndNotExistChannelId( - ErrorMessage.CHANNEL_NOT_FOUND, - channelId - )); - return foundChannel; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelService.java deleted file mode 100644 index 68e9f8c01..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFChannelService.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import static com.sprint.mission.discodeit.common.error.ErrorMessage.CHANNEL_NOT_FOUND; -import static com.sprint.mission.discodeit.common.error.ErrorMessage.USER_NOT_FOUND; - -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.channel.dto.ChangeChannelNameRequest; -import com.sprint.mission.discodeit.entity.channel.dto.ChannelResponse; -import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; -import com.sprint.mission.discodeit.entity.channel.dto.DeleteChannelRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.service.channel.ChannelConverter; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import java.util.UUID; - -public class JCFChannelService implements ChannelService { - - private final ChannelRepository channelRepository; - private final UserRepository userRepository; - private final ChannelConverter channelConverter; - - private JCFChannelService( - ChannelRepository channelRepository, - UserRepository userRepository, - ChannelConverter channelConverter - ) { - this.channelRepository = channelRepository; - this.userRepository = userRepository; - this.channelConverter = channelConverter; - } - - public static ChannelService getInstance(UserRepository userRepository, ChannelRepository channelRepository) { - return new JCFChannelService(channelRepository, userRepository, new ChannelConverter()); - } - - @Override - public ChannelResponse createChannelOrThrow(CreateNewChannelRequest request) { - /** - * ==> 코드리뷰 : 메서드 내부에 다른 메서드를 호출하면서 throw가 발생가능하다는 것을 코드 블록의 메서드의 이름에 추가해야할까요? - * createChannel VS createChannelOrThrow - */ - var findUser = findUserByIdOrThrow(request.userId()); - - var createdChannel = findUser.openNewChannel(request.channelName()); - - channelRepository.save(createdChannel); - userRepository.save(findUser); - - return channelConverter.toDto(createdChannel); - } - - @Override - public void changeChannelNameOrThrow(ChangeChannelNameRequest request) { - var foundUser = findUserByIdOrThrow(request.userId()); - - var changedChannel = foundUser.changeChannelName(request.channelId(), request.newChannelName()); - - channelRepository.save(changedChannel); - userRepository.save(foundUser); - } - - @Override - public void deleteChannelByChannelIdOrThrow(DeleteChannelRequest request) { - var foundChannel = channelRepository.findById(request.channelId()) - .orElseThrow( - () -> ChannelException.ofErrorMessageAndNotExistChannelId( - CHANNEL_NOT_FOUND, - request.channelId()) - ); - - var foundUser = findUserByIdOrThrow(request.userId()); - foundChannel.deleteChannel(foundUser); - - channelRepository.save(foundChannel); - } - - private User findUserByIdOrThrow(UUID id) { - var foundUser = userRepository.findById(id) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.of(USER_NOT_FOUND)); - - return foundUser; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFDirectMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFDirectMessageService.java deleted file mode 100644 index 2702cfbf7..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFDirectMessageService.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import com.sprint.mission.discodeit.common.error.ErrorMessage; -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.repository.jcf.message.directMessage.DirectMessageRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.message.DirectMessage; -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendDirectMessageRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.service.message.converter.DirectMessageConverter; -import com.sprint.mission.discodeit.service.message.directMessage.DirectMessageService; -import java.util.UUID; - -public class JCFDirectMessageService implements DirectMessageService { - - private final DirectMessageRepository directMessageRepository; - private final UserRepository userRepository; - private final DirectMessageConverter directMessageConverter; - - private JCFDirectMessageService( - DirectMessageRepository directMessageRepository, - UserRepository userRepository, - DirectMessageConverter directMessageConverter - ) { - this.directMessageRepository = directMessageRepository; - this.userRepository = userRepository; - this.directMessageConverter = directMessageConverter; - } - - public static DirectMessageService getInstance( - DirectMessageRepository directMessageRepository, - UserRepository userRepository - ) { - var converter = new DirectMessageConverter(); - return new JCFDirectMessageService(directMessageRepository, userRepository, converter); - } - - @Override - public DirectMessageInfoResponse sendMessage(SendDirectMessageRequest sendDirectMessageRequest) { - - var foundSendUser = findUserByIdOrThrow(sendDirectMessageRequest.sendUserId()); - var foundReceiverUser = findUserByIdOrThrow(sendDirectMessageRequest.receiveUserId()); - - var createdDirectMessage = DirectMessage.ofMessageAndSenderReceiver( - sendDirectMessageRequest.message(), - foundSendUser, - foundReceiverUser - ); - - var savedDirectMessage = directMessageRepository.save(createdDirectMessage); - - createdDirectMessage.sendMessage(); - - return directMessageConverter.toDto(savedDirectMessage); - } - - private User findUserByIdOrThrow(UUID userId) { - var foundUser = userRepository.findById(userId) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.ofErrorMessageAndId( - ErrorMessage.USER_NOT_FOUND, userId.toString() - )); - - return foundUser; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFUserService.java deleted file mode 100644 index b30e8fb70..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/jcf/JCFUserService.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import static com.sprint.mission.discodeit.common.error.ErrorMessage.USER_NOT_FOUND; - -import com.sprint.mission.discodeit.common.error.user.UserException; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.user.dto.ExitChannelRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.entity.user.dto.FindUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.ModifyUserInfoRequest; -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UnregisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; -import com.sprint.mission.discodeit.service.user.UserConverter; -import com.sprint.mission.discodeit.service.user.UserService; -import java.util.UUID; - -public class JCFUserService implements UserService { - - private final UserRepository userRepository; - private final UserConverter converter; - - public JCFUserService( - UserRepository userRepository, - UserConverter converter - ) { - this.userRepository = userRepository; - this.converter = converter; - } - - @Override - public UserInfoResponse register(RegisterUserRequest registerUserRequest) { - var entity = converter.toEntity(registerUserRequest); - var savedEntity = userRepository.save(entity); - - return converter.toDto(savedEntity); - } - - @Override - public UserInfoResponse findUserByUsernameOrThrow(FindUserRequest findUserRequest) { - var foundUser = userRepository.findByUsername(findUserRequest.username()) - .filter(User::isNotUnregistered) - .map(converter::toDto) - .orElseThrow(() -> UserException.of(USER_NOT_FOUND)); - - return foundUser; - } - - @Override - public UserInfoResponse modifyUserInfo(ModifyUserInfoRequest request) { - var foundUser = findByUserIdOrThrow(request.id()); - - foundUser.changeUserName(request.changeUsername()); - userRepository.save(foundUser); - - var response = converter.toDto(foundUser); - return response; - } - - @Override - public void UnRegisterUser(UnregisterUserRequest request) { - var foundUser = findByUserIdOrThrow(request.id()); - - foundUser.unregister(); - - userRepository.save(foundUser); - } - - @Override - public void exitChannel(ExitChannelRequest request) { - var foundUser = findByUserIdOrThrow(request.userId()); - foundUser.exitParticipatedChannel(request.channelId()); - userRepository.save(foundUser); - } - - private User findByUserIdOrThrow(UUID id) { - var entity = userRepository.findById(id) - .filter(User::isNotUnregistered) - .orElseThrow(() -> UserException.of(USER_NOT_FOUND)); - return entity; - } - - public static JCFUserService getInstance(UserRepository userRepository) { - return new JCFUserService(userRepository, new UserConverter()); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/channelMessage/ChannelMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/channelMessage/ChannelMessageService.java deleted file mode 100644 index 4578cc6ba..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/channelMessage/ChannelMessageService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sprint.mission.discodeit.service.message.channelMessage; - -import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendChannelMessageRequest; - -public interface ChannelMessageService { - - ChannelMessageInfoResponse sendMessage(SendChannelMessageRequest sendChannelMessageRequest); -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java deleted file mode 100644 index da9d6f802..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/ChannelMessageConverter.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sprint.mission.discodeit.service.message.converter; - -import com.sprint.mission.discodeit.entity.message.ChannelMessage; -import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.ChannelMessageInfoResponse.Builder; -import org.springframework.stereotype.Component; - -@Component -public class ChannelMessageConverter { - - public ChannelMessageInfoResponse toDto(ChannelMessage message) { - var response = new ChannelMessageInfoResponse.Builder() - .messageId(message.getId()) - .sendUserId(message.getMessageSender().getId()) - .receiveChannelId(message.getReceiverChannel().getId()) - .message(message.getMessage()) - .build(); - - return response; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java deleted file mode 100644 index 869eeb796..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/converter/DirectMessageConverter.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sprint.mission.discodeit.service.message.converter; - -import com.sprint.mission.discodeit.entity.message.DirectMessage; -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse.Builder; -import org.springframework.stereotype.Component; - -@Component -public class DirectMessageConverter { - - public DirectMessageInfoResponse toDto(DirectMessage directMessage) { - var response = new Builder() - .messageId(directMessage.getId()) - .sender(directMessage.getMessageSender().getId()) - .receiver(directMessage.getMessageReceiver().getId()) - .message(directMessage.getMessage()) - .build(); - - return response; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/directMessage/DirectMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/directMessage/DirectMessageService.java deleted file mode 100644 index 1eee17b5a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/message/directMessage/DirectMessageService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sprint.mission.discodeit.service.message.directMessage; - -import com.sprint.mission.discodeit.entity.message.dto.DirectMessageInfoResponse; -import com.sprint.mission.discodeit.entity.message.dto.SendDirectMessageRequest; - -public interface DirectMessageService { - - DirectMessageInfoResponse sendMessage(SendDirectMessageRequest sendDirectMessageRequest); -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java deleted file mode 100644 index 224217f7d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserConverter.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.sprint.mission.discodeit.service.user; - -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.util.Objects; -import org.springframework.stereotype.Component; - -@Component -public class UserConverter { - private static UserConverter INSTANCE; - - public UserConverter() { - } - - public static UserConverter getInstance() { - if (INSTANCE == null) { - INSTANCE = new UserConverter(); - } - return Objects.requireNonNull(INSTANCE); - } - - public User toEntity(RegisterUserRequest request) { - var createdUser = User.createFrom(request.name()); - return createdUser; - } - - public UserInfoResponse toDto(User user) { - var responseDto = new UserInfoResponse( - user.getId(), - user.getNicknameValue(), - user.getStatus() - ); - return responseDto; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserService.java deleted file mode 100644 index cea20b893..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/service/user/UserService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.sprint.mission.discodeit.service.user; - -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.user.dto.ExitChannelRequest; -import com.sprint.mission.discodeit.entity.user.dto.FindUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.ModifyUserInfoRequest; -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UnregisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UserInfoResponse; -import com.sprint.mission.discodeit.service.jcf.JCFUserService; - -public interface UserService { - - UserInfoResponse register(RegisterUserRequest registerUserRequest); - - UserInfoResponse findUserByUsernameOrThrow(FindUserRequest findUserRequest); - - UserInfoResponse modifyUserInfo(ModifyUserInfoRequest request); - - void UnRegisterUser(UnregisterUserRequest request); - - void exitChannel(ExitChannelRequest request); - - public static UserService getJCFUserService(UserRepository userRepository) { - return JCFUserService.getInstance(userRepository); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java deleted file mode 100644 index 1c51df754..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.sprint.mission.discodeit; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class DiscodeitApplicationTests { - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/UUIDTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/UUIDTest.java deleted file mode 100644 index 4bd085631..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/UUIDTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sprint.mission.discodeit; - -import static org.assertj.core.api.Assertions.*; - -import java.util.UUID; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -public class UUIDTest { - - @Test - void uuidTest() { - var uuid = UUID.randomUUID(); - var targetUUID = UUID.fromString(uuid.toString()); - - var retsult = uuid.equals(targetUUID); - - assertThat(retsult).isTrue(); - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java new file mode 100644 index 000000000..1ef1ca6ff --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java @@ -0,0 +1,32 @@ +package com.sprint.mission.discodeit.domain.user; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class EmailTest { + + @ParameterizedTest + @ValueSource(strings = { + "test@example.com", // ✅ 일반적인 이메일 + "user.name@domain.co.kr", // ✅ 도메인에 . 포함 가능 + "valid_email123@gmail.com", // ✅ 숫자, 밑줄 포함 가능 + "email.with+symbol@domain.com", // ✅ `+` 포함 가능 + "firstname.lastname@example.com", // ✅ 마침표(`.`) 포함 가능 + "email@sub.domain.com", // ✅ 서브 도메인 포함 가능 + "123456@example.org", // ✅ 숫자만 포함 가능 + "user_name@company.net", // ✅ 밑줄 포함 가능 + "email@domain.io", // ✅ `io` 같은 도메인 가능 + "valid-email@domain.info", // ✅ `-` 포함 가능 + "email@domain.tech", // ✅ 최신 TLD (예: `.tech`) 지원 + "abc.def@ghi.jkl", // ✅ 짧은 도메인 허용 + "valid-email@sub.domain.co.uk" // ✅ 여러 개의 서브도메인 가능 + }) + void 이메일_생성_성공(String email) { + //given + // when + Email createdEmail = new Email(email); + // then + Assertions.assertThat(createdEmail.getValue()).isEqualTo(email); + } +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java new file mode 100644 index 000000000..3dca6f1de --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java @@ -0,0 +1,36 @@ +package com.sprint.mission.discodeit.domain.user; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class NicknameTest { + + @ParameterizedTest + @ValueSource(strings = { + "User1", // 일반적인 영어 이름 + "유저123", // 한글 + 숫자 조합 + "emoji😊", // 영어 + 이모지 + "닉네임_테스트", // 한글 + 밑줄 + "user_name", // 영어 + 밑줄 + "유저_이름", // 한글 + 밑줄 + "테스트123", // 한글 + 숫자 조합 + "유저_😊", // 한글 + 이모지 + "123456789012345", // 최대 길이 15글자 + "😊😊😊😊😊", // 이모지만 사용 (최대 5글자 이모지) + "name_123", // 밑줄 + 숫자 조합 + "_under_score",// 밑줄로 시작 가능 여부 + "이름😊", // 한글 + 이모지 + "USERNAME", // 영어 대문자 + "User_😊_Name" // 영어 + 밑줄 + 이모지 + }) + void 유저_닉네임_생성_성공(String nickname) { + //given + // when + Nickname createNickname = new Nickname(nickname); + // then + assertThat(createNickname.getValue()).isEqualTo(nickname); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java new file mode 100644 index 000000000..ce1708b01 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java @@ -0,0 +1,30 @@ +package com.sprint.mission.discodeit.domain.user; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class UsernameTest { + + @ParameterizedTest + @ValueSource(strings = { + "user123", // ✅ 정상적인 영어+숫자 + "john_doe", // ✅ 밑줄 포함 허용 + "player.99", // ✅ 마침표 포함 허용 + "nickname_", // ✅ 밑줄(_) 끝 가능 + "_nickname", // ✅ 밑줄(_) 시작 가능 + "nickname.valid", // ✅ 밑줄 + 마침표 포함 허용 + "validUser123", // ✅ 대문자 포함 가능 + "user.name.123", // ✅ 여러 개의 마침표 허용 (연속된 마침표 제외) + "abcdefghijklmnopqrstuvwxyzabcd", // ✅ 32자 최대 허용 + }) + void 유저_이름_생성_성공(String username) { + //given + // when + Username createUsername = new Username(username); + // then + assertThat(createUsername.getValue()).isEqualTo(username); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java new file mode 100644 index 000000000..63d1a40ed --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java @@ -0,0 +1,39 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sprint.mission.discodeit.domain.user.exception.EmailInvalidException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class EmailValidatorTest { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { + "abc", // 일반 문자열 + "abc@", // '@'만 포함 + "@example.com", // 로컬 부분 없음 + "abc@example", // 최상위 도메인 없음 + "abc@.com", // 도메인 부분이 비정상적 + "abc@com.", // 도메인 형식 오류 + "abc@@example.com", // '@' 중복 + "abc example.com", // 공백 포함 + "abc@ex@ample.com", // 여러 개의 '@' + "abc@example..com", // 연속된 '.' 포함 + "abc@example.c", // 너무 짧은 TLD + "abc@example#com", // 특수 문자 포함 + "abc@.com", // 도메인 앞부분 없음 + "abc@exam_ple.com", // 언더스코어 포함 (일반적으로 허용되지 않음) + }) + void 이메일_생성_Invalid값_에러throw(String email) { + //given + // when + Throwable throwable = Assertions.catchThrowable(() -> EmailValidator.valid(email)); + // then + assertThat(throwable).isInstanceOf(EmailInvalidException.class); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java new file mode 100644 index 000000000..3b7bdad9d --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java @@ -0,0 +1,46 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.junit.jupiter.api.Assertions.*; + +import com.sprint.mission.discodeit.domain.user.Nickname; +import com.sprint.mission.discodeit.domain.user.exception.NickNameInvalidException; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class NicknameValidatorTest { + + @ParameterizedTest + @NullAndEmptySource + @MethodSource("nicknameOverLengthProvider") + @ValueSource(strings = { + "admin", // 금지된 단어 + "moderator", // 금지된 단어 + "discord123", // 금지된 단어 포함 + "superadmin", // 금지된 단어 포함 + "mod🚀", // 금지된 단어 포함 + 이모지 (이모지는 가능하지만 금지어 포함이라 제한) + "hello admin!", // 금지된 단어 포함 + "AdminUser", // 금지된 단어 포함 (대소문자 무시) + "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) + "root_user", // 금지된 단어 포함 (관리자 권한 관련) + "system_mod", // 금지된 단어 포함 + "운영자", // 한글 운영자 금지 + "테스트관리자", // 한글 관리자 관련 단어 포함 + "봇", // 봇 계정 금지 + }) + void 유저닉네임_제한되는_입력값_에러검증throw(String nickname) { + //given + // when + Throwable catchThrowable = catchThrowable(() -> new Nickname(nickname)); + // then + assertThat(catchThrowable).isInstanceOf(NickNameInvalidException.class); + } + + static Stream nicknameOverLengthProvider() { + return Stream.of("a".repeat(33)); + } +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java new file mode 100644 index 000000000..503782dd9 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java @@ -0,0 +1,53 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import static org.junit.jupiter.api.Assertions.*; + +import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class UsernameValidatorTest { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { + "a", // 너무 짧음 (최소 2자) + "abcdefghijklmnopqrstuvwxyzabcdefghi", // 너무 김 (32자 초과, 33자) + "user name", // 공백 포함 (허용되지 않음) + "🔥coolgamer", // 이모지 포함 (정규식이 허용하지 않음) + "user!name", // 특수문자 포함 (! 허용되지 않음) + "....hidden....", // 연속된 마침표 (허용되지 않음) + ".username", // 마침표로 시작 (허용되지 않음) + "username.", // 마침표로 끝남 (허용되지 않음) + "admin", // 금지된 단어 포함 + "moderator", // 금지된 단어 포함 + "discord123", // 금지된 단어 포함 + "superadmin", // 금지된 단어 포함 + "mod🚀", // 금지된 단어 포함 + 이모지 + "hello admin!", // 금지된 단어 포함 + "AdminUser", // 금지된 단어 포함 (대소문자 무시) + "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) + "root_user", // 금지된 단어 포함 (관리자 권한 관련) + "system_mod", // 금지된 단어 포함 + "운영자", // 한글 운영자 금지 + "테스트관리자", // 한글 관리자 관련 단어 포함 + "봇", // 봇 계정 금지 + }) + @MethodSource("usernameOverLengthProvider") + void testMethodNameHere(String username) { + //given + // when + Throwable catchThrowable = Assertions.catchThrowable(() -> UsernameValidator.validate(username)); + // then + Assertions.assertThat(catchThrowable).isInstanceOf(UserNameInvalidException.class); + } + + static Stream usernameOverLengthProvider() { + return Stream.of("a".repeat(33)); + } +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/channel/ChannelTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/channel/ChannelTest.java deleted file mode 100644 index c95c98fe8..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/channel/ChannelTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.sprint.mission.discodeit.entity.channel; - -import static com.sprint.mission.discodeit.entity.common.Status.MODIFIED; -import static com.sprint.mission.discodeit.entity.common.Status.UNREGISTERED; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.sprint.mission.discodeit.entity.user.entity.User; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -@DisplayName("채널 도메인") -class ChannelTest { - private static final String CHANNEL_NAME = "스프린트_백엔드_1기"; - private static final String USER_NAME = "SB_1기_백재우"; - private static final User USER = User.createFrom(USER_NAME); - private Channel channel; - - @Test - @DisplayName("새로운 채널 생성") - void createChannelThenSuccess() { - assertThat(Channel.createOfChannelNameAndUser(CHANNEL_NAME, USER)) - .isNotNull(); - } - - @Nested - @DisplayName("메서드 검사") - class whenUseMethodTest { - - @BeforeEach - void setUp() { - channel = Channel.createOfChannelNameAndUser(CHANNEL_NAME, USER); - } - - @Test - @DisplayName("변경하려는 새로운 이름으로 채널이름 변경하면 채널의 이름과 상태가 변경") - void givenChangeChannelNameWhenChangeNameThenUpdateStatusAndChannelName() { - // given - var newChannelName = "SB_999기_백재우"; - // then - channel.changeName(newChannelName, USER); - // when - assertAll( - () -> { - assertThat(channel.getChannelName()).isNotEqualTo(CHANNEL_NAME); - assertThat(channel.getStatus()).isEqualTo(MODIFIED); - } - ); - } - - @Test - @DisplayName("채널을 삭제 시 상태가 변경") - void givenChannelWhenDeleteChannelThenStatusUpdate() { - // given - // when - channel.deleteChannel(USER); - // then - assertThat(channel.getStatus()).isEqualTo(UNREGISTERED); - } - - @Test - @DisplayName("매개변수로 들어온 이름과 채널 객체의 이름이 같을 경우, 상태가 해지 상태가 아닐 시 True 반환") - void givenSameChannelNameWhenIsEqualFromNameAndNotUnregisteredThenTrue() { - // given - - // when - var validResult = channel.isStatusNotUnregisteredAndEqualsTo(CHANNEL_NAME); - // then - assertThat(validResult).isTrue(); - } - - @Test - @DisplayName("매개변수로 들어온 이름과 채널 객체의 이름이 다를 경우, 상태가 해지 상태가 아닐 시 False 반환") - void givenDifferentChannelNameWhenIsEqualFromNameAndNotUnregisteredThenFalse() { - // given - var differentChannelName = "스프린트_백엔드_100기"; - // when - var validResult = channel.isStatusNotUnregisteredAndEqualsTo(differentChannelName); - // then - assertThat(validResult).isFalse(); - } - - @Test - @DisplayName("찾으려는 채널이름과 비교하려는 채널 객체의 이름이 같고 해지상태가 아닐 경우 true 반환") - void givenSameNameWhenIsStatusNotUnregisteredAndEqualsToThenTrue() { - // given - var findChannelNameRequest = CHANNEL_NAME; - // when - var isEquals = channel.isStatusNotUnregisteredAndEqualsTo(findChannelNameRequest); - // then - assertThat(isEquals).isTrue(); - } - - @Test - @DisplayName("찾으려는 채널이름과 비교하려는 채널 객체 이름이 같고 해지상태일 경우 false 반환") - void givenSameNameAndUnregisteredWhenIsStatusNotUnregisteredThenFalse() { - // given - var findChannelNameRequest = CHANNEL_NAME; - channel.updateUnregistered(); - // when - var isEquals = channel.isStatusNotUnregisteredAndEqualsTo(findChannelNameRequest); - // then - assertThat(isEquals).isFalse(); - } - } -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java deleted file mode 100644 index c0e565839..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/common/AbstractUUIDTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.sprint.mission.discodeit.entity.common; - -import static com.sprint.mission.discodeit.entity.common.Status.MODIFIED; -import static com.sprint.mission.discodeit.entity.common.Status.REGISTERED; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import com.sprint.mission.discodeit.testdummy.TestUUIDEntity; -import java.lang.reflect.Field; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public class AbstractUUIDTest { - - private BaseEntity entity; - - - @BeforeEach - void init() { - entity = new TestUUIDEntity(); - } - - @Test - @DisplayName("공통 엔티티 추상 클래스 생성 시 id, createdAt, status 필드 초기화 시 Not Null test") - void createAbstractUUIDThenInitializeFieldIdAndCreatedAtAndStatusIsNotNullTest() { - assertAll( - () -> assertThat(entity.getId()).as("Id should not be null").isNotNull(), - () -> assertThat(entity.getCreateAt()).as("createAt should not be null").isNotNull(), - () -> assertThat(entity.getStatus()).isEqualTo(REGISTERED) - ); - } - - @Test - @DisplayName("엔티티 내 데이터가 수정 시 updateAt and status 필드 수정 여부 테스트") - void givenWhenSomeDateModifyThenFieldUpdateAtAndStatusIsChangedTest() { - // given - // when - entity.updateStatusAndUpdateAt(); - //then - assertAll( - () -> assertThat(entity.getUpdateAt()).as("invoke update() then updateAt must be present"), - () -> assertThat(entity.getStatus()).isEqualTo(MODIFIED) - ); - } - - @Test - @DisplayName("동일한 UUID를 가진 객체간의 동등성 비교를 할 경우 True를 반환하는지 테스트") - void givenCreateAbstractEntityAndEqualEntityWhenIsEqualsThenReturnTrueTest() throws Exception { - // given - BaseEntity entity1 = new TestUUIDEntity(); - BaseEntity entity2 = new TestUUIDEntity(); - - UUID sameUUID = UUID.randomUUID(); - - setFinalField(entity1, "id", sameUUID); - setFinalField(entity2, "id", sameUUID); - - // then - assertEquals(entity1, entity2, "Entities with the same UUID should be equal"); - - BaseEntity entity3 = new TestUUIDEntity(); - assertNotEquals(entity1, entity3, "Entities with different UUIDs should not be equal"); - } - - // 리플렉션. 이 부분 코드 리뷰 XXX - private void setFinalField(Object object, String fieldName, Object value) throws Exception { - Field field = object.getClass().getSuperclass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(object, value); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserNameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserNameTest.java deleted file mode 100644 index 3880736b3..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserNameTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.sprint.mission.discodeit.entity.user; - -import static com.sprint.mission.discodeit.entity.user.entity.UserName.NAME_MAX_LENGTH; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchNullPointerException; -import static org.assertj.core.api.BDDAssertions.then; - -import com.sprint.mission.discodeit.entity.user.entity.UserName; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import java.util.Set; -import java.util.stream.Stream; -import net.bytebuddy.asm.Advice.Thrown; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.NullSource; - -class UserNameTest { - - private UserName userName; - - private Validator validator; - - @BeforeEach - void setUp() { - validator = Validation.buildDefaultValidatorFactory().getValidator(); - } - - static Stream stringProvider() { - return Stream.of( - ".".repeat(NAME_MAX_LENGTH + 1), - "T", - "TT" - ); - } - - @ParameterizedTest(name = "[test {index}] ==> given name : {arguments}") - @NullSource - @DisplayName("요구되는 유저의 이름이 비어있는 value 제공 시 에러 발생 테스트") - void givenUserNameLengthLessThanRequiredLengthWhenCreateUserThenThrowException(String name) { - // given - -// Set> violations = validator.validate(userName); - - // when - Throwable thrown = catchNullPointerException(() -> userName = UserName.createFrom(name)); - // then - assertThat(thrown).isInstanceOf(NullPointerException.class); -// assertThat(violations.size()).isEqualTo(1); - } - - @ParameterizedTest(name = "given name : {arguments}") - @MethodSource("stringProvider") - @DisplayName("유저의 이름의 제한을 넘어가는 문자열로 생성 시 에러 발생 테스트") - void givenMoreThanInvalidLengthNameWhenCreateUserNameThenThrowException(String overLengthName) { - // given - userName = UserName.createFrom(overLengthName); - Set> violations = validator.validate(userName); - // then - assertThat(violations.size()).isEqualTo(1); - } - - - @Nested - @DisplayName("초기화 후 기능 테스트") - class whenSetup { - - private static final String USER_NAME = "SB_1기_백재우"; - - @BeforeEach - void setup() { - userName = UserName.createFrom(USER_NAME); - } - - @Test - @DisplayName("특정 문자열을 통해 UserName 객체 생성 후 동일한 문자열을 리턴 테스트") - void givenNameWhenCreateUserNameThenReturnUserName() { - then(userName.getName()).isEqualTo(USER_NAME); - } - - @Test - @DisplayName("새로운 이름으로 사용자 이름을 변경 시 새로운 UserName 객체 반환") - void givenNewUserNameWhenChangeUserNameThenReturnNewUserName() { - // given - String newName = "SB_2기_백재우"; - // when - var newUserName = userName.changeName(newName); - // then - then(newUserName).isNotEqualTo(userName); - } - } -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java deleted file mode 100644 index 198cef97b..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/UserTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.sprint.mission.discodeit.entity.user; - -import static com.sprint.mission.discodeit.entity.common.Status.UNREGISTERED; -import static org.assertj.core.api.Assertions.assertThat; - -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -@DisplayName("User Test") -class UserTest { - private static final String USER_NAME = "test"; - private static final String CHANNEL_NAME = "코드잇-스프린트_1기"; - private User user; - - @Test - void createUserTest() { - // given - String userName = "test1"; - - assertThat(User.createFrom(userName)).isNotNull(); - } - - @Nested - @DisplayName("유저 생성 수정 삭제") - class InitializeNew { - @BeforeEach - void setUp() { - user = User.createFrom(USER_NAME); - } - - @Test - @DisplayName("유저 해지 요청 후 유저의 상태 변경") - void givenUnregisterRequestWhenUnregisterThenStatusChange() { - // given - // when - user.unregister(); - // then - assertThat(user.getStatus()).isEqualTo(UNREGISTERED); - } - } - - @Nested - @DisplayName("유저 채널") - class aboutChannel { - private Channel participatedChannel; - - @BeforeEach - void setup() { - user = User.createFrom(USER_NAME); - participatedChannel = user.openNewChannel("new Channel1"); - } - - @Test - @DisplayName("채널 이름을 유저가 제공하여 새로운 채널을 만들면 생성된 채널 반환") - void givenChannelNameWhenUserCreateChannelThenReturnChannel() { - // given - var createdUser = User.createFrom(USER_NAME); - // when - var channel = createdUser.openNewChannel(CHANNEL_NAME); - // then - Assertions.assertAll( - () -> { - assertThat(channel).isNotNull(); - assertThat(channel.getChannelName()).isEqualTo(CHANNEL_NAME); - } - ); - } - - @Test - @DisplayName("새로운 채널 이름으로 유저가 채널의 이름을 변경 시 변경된 채널 반환") - void givenChangeChannelNameWhenUserChangeChannelNameThenReturnChangedChannel() { - // given - var newChangeChannelName = "NEW CHANNEL NAME"; - var targetChannelId = participatedChannel.getId(); - // when - var changeChannel = user.changeChannelName(targetChannelId, newChangeChannelName); - // then - assertThat(changeChannel.getChannelName()).isEqualTo(newChangeChannelName); - } - - @Test - @DisplayName("유저의 참가 채널 id로 참가한 채널에서 나가면 참가채널 수 감소") - void givenExitChannelIdWhenExitParticipatedChannelThenReturnParticipatedChannelCountDecrease() { - // given - var participatedChannelId = participatedChannel.getId(); - var oldCount = user.countParticipatedChannels(); - // when - user.exitParticipatedChannel(participatedChannelId); - // then - assertThat(user.countParticipatedChannels()).isEqualTo(oldCount - 1); - } - } - -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java deleted file mode 100644 index 9792ff08f..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/EmailTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.domain; - -import static org.assertj.core.api.Assertions.*; - -import com.sprint.mission.discodeit.domain.user.Email; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -class EmailTest { - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {"test.com"}) - void 잘못된_이메일_형식_생성시_에러throw(String email) { - //given - // when - Throwable catchThrow = catchThrowable(() -> new Email(email)); - // then - assertThat(catchThrow).isInstanceOf(IllegalArgumentException.class); - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java deleted file mode 100644 index 5b83dfd2c..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/domain/NicknameTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.domain; - -import static org.assertj.core.api.Assertions.*; - -import com.sprint.mission.discodeit.domain.user.Nickname; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -class NicknameTest { - - @ParameterizedTest - @NullAndEmptySource - void 유저_닉네임_생성_null값으로생성_에러throw(String value) { - //given - // when - Throwable catchThrowable = catchThrowable(() -> new Nickname(value)); - // then - assertThat(catchThrowable).isInstanceOf(IllegalArgumentException.class); - } - - @Test - void testMethodNameHere() { - //given - String nickname = "test"; - Nickname nickname1 = new Nickname(nickname); - Nickname nickname2 = new Nickname(nickname); - // when - boolean result = nickname1.equals(nickname2); - - // then - assertThat(result).isTrue(); - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java deleted file mode 100644 index ba64897d3..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/entity/user/entity/ParticipatedChannelTest.java +++ /dev/null @@ -1,220 +0,0 @@ -package com.sprint.mission.discodeit.entity.user.entity; - -import static com.sprint.mission.discodeit.common.error.ErrorMessage.USER_NOT_PARTICIPATED_CHANNEL; -import static com.sprint.mission.discodeit.entity.common.Status.MODIFIED; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.sprint.mission.discodeit.common.error.channel.ChannelException; -import com.sprint.mission.discodeit.entity.channel.Channel; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class ParticipatedChannelTest { - private static final String CHANNEL_NAME = "코드잇-1기-백엔드"; - private ParticipatedChannel channels; - private static final User USER = User.createFrom("SB_1기_백재우"); - - @Test - void givenNewParticipatedChannelWhenCreateThenReturnNewParticipatedChannel() { - // given when - var createdParticipatedChannel = ParticipatedChannel.newDefault(); - // then - assertThat(createdParticipatedChannel).isNotNull(); - } - - @BeforeEach - void setUp() { - channels = ParticipatedChannel.newDefault(); - } - - @Test - void givenChannelNameWhenCreateChannelThenReturnNewChannel() { - // given when - var channel = channels.createChannel(CHANNEL_NAME, USER); - // then - assertAll( - () -> { - assertThat(channel).isNotNull(); - - assertAll( - () -> assertThat(channel.getChannelName()).isEqualTo(CHANNEL_NAME) - ); - } - ); - } - - @Nested - @DisplayName("유저의 참여 채널") - class givenSomeElements { - private static final String addChannelName1 = "스프린트_2기_백엔드"; - private static final String addChannelName2 = "스프린트_3기_백엔드"; - private Channel createdChannel1; - private Channel createdChannel2; - - @BeforeEach - void add() { - createdChannel1 = channels.createChannel(addChannelName1, USER); - createdChannel2 = channels.createChannel(addChannelName2, USER); - } - - @Test - @DisplayName("유저가 참여한 채널에서 Id를 통해 채널 조회") - void givenSomeElementsAddedChannelsWhenFindByIdThenReturnSomeElements() { - // given - var channelId = createdChannel1.getId(); - - // when - var findByIdParticipatedChannel = channels.findByChannelIdNotUnregisteredOrThrow(channelId); - - // then - assertAll( - () -> { - assertThat(findByIdParticipatedChannel).isNotNull(); - - assertAll( - () -> assertThat(findByIdParticipatedChannel.orElseThrow().getId()) - .isEqualTo(channelId) - ); - } - ); - } - - @Test - @DisplayName("유저가 참여한 채널에서 channelName 을 통해 채널 조회") - void givenSomeElementsAddedChannelsWhenFindByNameThenReturnSomeElements() { - // given - var channelName = createdChannel1.getChannelName(); - - // when - var findByIdParticipatedChannel = channels.findByName(channelName); - - // then - assertAll( - () -> { - assertThat(findByIdParticipatedChannel).isNotNull(); - - assertAll( - () -> assertThat(findByIdParticipatedChannel.orElseThrow().getChannelName()) - .isEqualTo(addChannelName1) - ); - } - ); - } - - @Test - @DisplayName("유저가 참여한 채널에 존재하지 않는 channel Id로 채널 조회 시 에러 발생") - void givenNotExistChannelIdWhenFindByIdThenThrowException() { - // given - var notExistedChannelId = UUID.randomUUID(); - - // when - var throwable = catchThrowable(() -> - channels.findByChannelIdNotUnregisteredOrThrow(notExistedChannelId) - ); - - // then - assertThat(throwable).isInstanceOf(ChannelException.class) - .hasMessageContaining(USER_NOT_PARTICIPATED_CHANNEL.getMessage()); - } - - @Test - @DisplayName("유저가 참여한 채널에 존재하는 ChannelName 으로 채널 조회 시 해당 채널 반환") - void givenExistedChannelNameWhenFindByNameThenReturnChannel() { - // given - - // when - var foundChannel = channels.findByName(addChannelName1); - // then - assertThat(foundChannel) - .isNotNull(); - } - - @Test - @DisplayName("유저가 참여한 채널에 존재하지 않는 ChannelName 으로 채널 조회시 에러") - void givenNotExistedChannelNameWhenFindByNameThenThrowException() { - // given - var notExistedChannelName = "SB_999기_백재우"; - - // when - var foundChannel = channels.findByName(notExistedChannelName); - - // then - assertThat(foundChannel.orElse(null)).isNull(); - } - - @Test - @DisplayName("유저가 참여한 채널의 id로 채널이름을 변경 성공") - void givenParticipatedChannelIdAndNewChannelNameWhenChangeChannelNameThenSuccess() { - // given - var channelId = createdChannel1.getId(); - var changeChannelName = createdChannel1.getChannelName(); - - // when - channels.changeChannelNameOrThrow(channelId, "new ChannelName", USER); - - // then - assertAll( - () -> { - assertThat(createdChannel1.getChannelName()).isNotEqualTo(changeChannelName); - assertThat(createdChannel1.getStatus()).isEqualTo(MODIFIED); - } - ); - } - - @Test - @DisplayName("존재하지 않는 채널 id로 유저가 참여한 채널의 id로 채널이름 변경 시 에러") - void givenNotExistedChannelIdWhenChangeChannelNameThenThrowUserException() { - // given - var notExistedChannelId = UUID.randomUUID(); - var changeChannelName = "new ChannelName"; - - // when - var throwable = catchThrowable(() -> channels.changeChannelNameOrThrow(notExistedChannelId, changeChannelName, USER)); - - // then - assertThat(throwable).isInstanceOf(ChannelException.class) - .hasMessageContaining(notExistedChannelId.toString()); - - } - - @Test - @DisplayName("채널을 생성한 사람이 아닌 다른 누군가 채널이름을 변경하려고 할 경우 에러") - void givenNotChannelCreatorWhenChangeChannelNameThenThrowChannelException() { - // given - var notCreatorUser = User.createFrom("is not Creator"); - var changeChannelName = "SB_백엔드_1000기"; - var channelId = createdChannel1.getId(); - - // when - var throwable = catchThrowable(() -> - channels.changeChannelNameOrThrow(channelId, changeChannelName, notCreatorUser) - ); - - // then - assertThat(throwable).isInstanceOf(ChannelException.class) - .hasMessageContaining(notCreatorUser.getNicknameValue()); - } - - @Test - @DisplayName("채널 id 값으로 유저가 참여한 채널을 삭제하면 유저가 참여한 채널 목록의 개수 감소") - void givenDeleteTargetIdWhenDeleteChannelThenParticipatedChannelIsRemove() { - // given - var oldParticipatedChannelCount = channels.countParticipatedChannels(); - var deleteTargetChannelId = createdChannel2.getId(); - - // when - channels.exitChannelById(deleteTargetChannelId); - - // then - assertThat(channels.countParticipatedChannels()).isNotEqualTo(oldParticipatedChannelCount); - } - } -} - - - diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepositoryTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepositoryTest.java deleted file mode 100644 index 533989b2c..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/common/InMemoryCrudRepositoryTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.sprint.mission.discodeit.repository.common; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.sprint.mission.discodeit.testdummy.TestDummyInMemoryCurdRepository; -import com.sprint.mission.discodeit.testdummy.TestUUIDEntity; -import java.util.UUID; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -@DisplayName("레포지토리 추상 클래스 테스트") -class InMemoryCrudRepositoryTest { - - private static TestDummyInMemoryCurdRepository repository; - - @Test - void initializeRepositoryTest() { - assertThat(new TestDummyInMemoryCurdRepository()).isNotNull(); - } - - @Nested - @DisplayName("데이터를 저장했을 때") - class whenAddEntity { - - @BeforeEach - void setUp() { - repository = new TestDummyInMemoryCurdRepository(); - } - - @Test - @DisplayName("새로운 객체를 생성하여 저장했을 때 동일한 객체가 리턴") - void givenNewEntityWhenSaveEntityThenReturnNewEntity() { - // given - TestUUIDEntity newEntity = new TestUUIDEntity(); - // when - var savedEntity = repository.save(newEntity); - // then - assertThat(savedEntity).isEqualTo(newEntity); - } - - @Test - @DisplayName("레포지토리에 저장 시 총 저장되어 있는 개수 증가") - void givenSaveNewEntityWhenCountRepositoryThenSizeIncrease() { - // given - var oldCount = repository.count(); - repository.save(new TestUUIDEntity()); - // when - var newCount = repository.count(); - // then - assertThat(newCount).isEqualTo(oldCount + 1); - } - - @Test - @DisplayName("저장한 객체와 동일한 객체의 id로 가져온 객체 비교") - void givenSavedEntityWhenFindByIdThenReturnEqualIsTrue() { - // given - var createdEntity = new TestUUIDEntity(); - repository.save(createdEntity); - // when - var findEntity = repository.findById(createdEntity.getId()).orElse(null); - //then - assertThat(findEntity).isEqualTo(createdEntity); - } - - @Test - @DisplayName("새로운 객체를 저장 후 삭제 시 정상 작동") - void givenNewEntitySaveWhenDeleteEntityThenStoreSizeNoChange() { - // given - int storeSize = repository.count(); - var newEntity = new TestUUIDEntity(); - repository.save(newEntity); - // when - repository.deleteById(newEntity.getId()); - // then - assertThat(repository.count()).isEqualTo(storeSize); - } - } - - @Nested - @DisplayName("저장된 객체") - class whenFindEntity { - private static final int REPOSITORY_SIZE = 10; - private static TestUUIDEntity entity; - - @BeforeAll - static void setUp() { - repository = new TestDummyInMemoryCurdRepository(); - entity = new TestUUIDEntity(); - repository.save(entity); - for (int i = 0; i < REPOSITORY_SIZE; i++) { - repository.save(new TestUUIDEntity()); - } - } - - @Test - @DisplayName("확실히 저장되어 있는 객체를 id로 레포지터리에서 찾을 때 true 리턴") - void givenEnsureEntityIdWhenFindByIdThenReturnTrue() { - // given - var existId = entity.getId(); - // when - var result = repository.isExistsById(existId); - // then - assertThat(result).isTrue(); - } - - @Test - @DisplayName("존재하지 않는 id로 레포지터리에서 찾을 때 false 리턴") - void givenGenerateUUIDWhenFindByIdThenReturnFalse() { - // given - var id = UUID.randomUUID(); - // when - var result = repository.isExistsById(id); - //then - assertThat(result).isFalse(); - } - - // 모든 저장된 객체를 가져오기 - @Test - @DisplayName("저장된 모든 객체를 리스트로 조회시 레포지터리 count 메서드의 결과 비교") - void whenFindAllThenReturnAll() { - // given - int repositorySize = repository.count(); - // when - var allEntity = repository.findAll(); - // then - assertAll( - () -> assertThat(allEntity).isNotEmpty(), - () -> assertThat(allEntity.size()).isEqualTo(repositorySize) - ); - } - } -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/UserRepositoryImplTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/UserRepositoryImplTest.java deleted file mode 100644 index 0e677848c..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/UserRepositoryImplTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.sprint.mission.discodeit.repository.user; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.testdummy.TestDummyFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class UserRepositoryImplTest { - private static final String TEST_NAME = "Test"; - private User user; - private UserRepository userRepository; - - @BeforeEach - void setUp() { - userRepository = TestDummyFactory.getUserRepository(); - user = User.createFrom(TEST_NAME); - userRepository.save(user); - } - - - @Test - @DisplayName("사용자 이름으로 찾을 때 유저정보 객체를 리턴") - void givenUsernameWhenFindByNameThenReturnUser() { - // given - // when - var findUser = userRepository.findByUsername(TEST_NAME).orElse(null); - // then - assertAll( - () -> assertThat(findUser).isEqualTo(user), - () -> assertThat(findUser).isNotNull() - ); - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFChannelServiceTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFChannelServiceTest.java deleted file mode 100644 index 296300e59..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFChannelServiceTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.sprint.mission.discodeit.repository.jcf.channel.ChannelRepository; -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.channel.Channel; -import com.sprint.mission.discodeit.entity.channel.dto.CreateNewChannelRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.service.channel.ChannelConverter; -import com.sprint.mission.discodeit.service.channel.ChannelService; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -class JCFChannelServiceTest { - private ChannelService channelService; - @Mock - private ChannelRepository channelRepository; - @Mock - private UserRepository userRepository; - - private ChannelConverter channelConverter = new ChannelConverter(); - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - - channelService = JCFChannelService.getInstance(userRepository, channelRepository); - } - - - // createChannelOrThrow 테스트 => uuid 값이 수정할 수 없어서 테스트하는데 너무 어려움을 느낌.. - @Test - void test() { - // given - var user = User.createFrom("SB_1기_백재우"); - var channelName = "스프링백엔드_1기"; - var channel = Channel.createOfChannelNameAndUser(channelName, user); - // when - var request = new CreateNewChannelRequest(user.getId(), channelName); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - // then - channelService.createChannelOrThrow(request); - - verify(userRepository, times(1)).findById(user.getId()); - } -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFUserServiceTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFUserServiceTest.java deleted file mode 100644 index 0300965b4..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/service/jcf/JCFUserServiceTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import static com.sprint.mission.discodeit.entity.common.Status.MODIFIED; -import static com.sprint.mission.discodeit.entity.common.Status.REGISTERED; -import static com.sprint.mission.discodeit.entity.common.Status.UNREGISTERED; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.entity.user.dto.FindUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.ModifyUserInfoRequest; -import com.sprint.mission.discodeit.entity.user.dto.RegisterUserRequest; -import com.sprint.mission.discodeit.entity.user.dto.UnregisterUserRequest; -import com.sprint.mission.discodeit.entity.user.entity.User; -import com.sprint.mission.discodeit.service.user.UserConverter; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("UserInterface smoke testing") -class JCFUserServiceTest { - private static final String NAME = "SB_1기_백재우"; - - private UserRepository userRepository; - private UserConverter userConverter; - private JCFUserService userService; - private User user; - - @BeforeEach - void setUp() { - userRepository = mock(UserRepository.class); - userConverter = UserConverter.getInstance(); - userService = new JCFUserService(userRepository, userConverter); - user = User.createFrom(NAME); - } - - @Test - @DisplayName("유저 등록 userService register 호출 시 userInfoResponse 반환") - void givenNewUserRequestWhenRegisterThenReturnUserInfoResponse() { - // given - RegisterUserRequest registerUserRequest = new RegisterUserRequest(NAME); - var registerUser = userConverter.toEntity(registerUserRequest); - when(userRepository.save(any(User.class))).thenReturn(registerUser); - - // when - var infoResponse = userService.register(registerUserRequest); - - // then - assertAll( - () -> { - assertThat(infoResponse).isNotNull(); - - assertAll( - () -> assertThat(infoResponse.username()).isEqualTo(NAME), - () -> assertThat(infoResponse.status()).isEqualTo(REGISTERED) - ); - } - ); - verify(userRepository).save(any(User.class)); - } - - @Test - @DisplayName("유저 이름으로 findUserByUsername 호출 시 UserResponse 반환") - void givenUsernameWhenFindUserByUsernameThenReturnUserInfoResponse() { - // given - Optional mockUser = Optional.of(user); - var findUserRequest = new FindUserRequest(NAME); - when(userRepository.findByUsername(NAME)).thenReturn(mockUser); - - // when - var user = userService.findUserByUsernameOrThrow(findUserRequest); - - // then - assertThat(user).isNotNull(); - assertThat(user.username()).isEqualTo(NAME); - - verify(userRepository).findByUsername(NAME); - } - // TODO : 해지된 유지이름으로 조회 시 실패 테스트 - - @Test - @DisplayName("유저 정보 수정 modifyUserInfo 호출 시 UserResponse 반환") - void givenModifyUserInfoRequestWhenModifyUserInfoThenReturnUserInfoResponse() { - // given - var modifyUserInfoRequest = new ModifyUserInfoRequest(user.getId(), "CHANGE NAME"); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - when(userRepository.save(any(User.class))).thenReturn(user); - - // when - var userInfoResponse = userService.modifyUserInfo(modifyUserInfoRequest); - - // then - assertAll( - () -> { - assertThat(userInfoResponse).isNotNull(); - - assertAll( - () -> assertThat(userInfoResponse.status()).isEqualTo(MODIFIED), - () -> assertThat(userInfoResponse.username()).isEqualTo("CHANGE NAME") - ); - } - ); - - verify(userRepository).save(any(User.class)); - verify(userRepository).findById(user.getId()); - } - - // 탈퇴 - @Test - @DisplayName("유저 회원 탈퇴 unregister 호출 시 유저 상태 변경") - void givenUnregisterUserRequestWhenUnregisterThenUserStatusIsChanged() { - // given - var unregisterUserRequest = new UnregisterUserRequest(user.getId(), NAME); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - when(userRepository.save(any(User.class))).thenReturn(user); - - // when - userService.UnRegisterUser(unregisterUserRequest); - - // then - assertThat(user.getStatus()).isEqualTo(UNREGISTERED); - verify(userRepository).findById(user.getId()); - verify(userRepository).save(any(User.class)); - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyFactory.java deleted file mode 100644 index 1900c89da..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sprint.mission.discodeit.testdummy; - -import com.sprint.mission.discodeit.repository.jcf.user.UserRepository; -import com.sprint.mission.discodeit.repository.jcf.user.JCFUserRepositoryInMemory; -import com.sprint.mission.discodeit.entity.user.entity.User; -import java.util.ArrayList; -import java.util.List; - -public class TestDummyFactory { - - private static final List users = new ArrayList<>(List.of( - User.createFrom("홍길동"), - User.createFrom("김길동"), - User.createFrom("이길동"), - User.createFrom("박깅동") - )); - - - public static UserRepository getUserRepository() { - var userRepository = JCFUserRepositoryInMemory.getInstance(); - users.forEach(userRepository::save); - return userRepository; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyInMemoryCurdRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyInMemoryCurdRepository.java deleted file mode 100644 index 4cf3aa25e..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestDummyInMemoryCurdRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sprint.mission.discodeit.testdummy; - -import com.sprint.mission.discodeit.repository.common.InMemoryCrudRepository; -import java.util.UUID; - -public class TestDummyInMemoryCurdRepository extends InMemoryCrudRepository { -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java deleted file mode 100644 index 2fd8a5f6a..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/testdummy/TestUUIDEntity.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.sprint.mission.discodeit.testdummy; - -import com.sprint.mission.discodeit.entity.common.BaseEntity; - -public class TestUUIDEntity extends BaseEntity { -} diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" new file mode 100644 index 000000000..20505769a --- /dev/null +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -0,0 +1,24 @@ +# Domain Modeling +디스코드 서비스를 활용하면서 각 도메인 모델에 필요한 정보를 도출한다. + + +- User +- Channel +- Message + +## 유저 + +- 회원 가입 + - `이메일`, `별명`, `사용자명`, `비밀번호`, `생년월일`, `수신여부` 를 입력받는다. + - 별명 : 다른 회원에게 보여지는 이름입니다. 이모지 사용가능합니다. + - 서버마다 다르게 설정가능 + - 최소 1~ 최대 32자 + - 사용자명: 숫자,밑줄 _, 마침표, 문자 + - 길이제한: 최소 2 ~ 최대 32자 + - 사용가능문자 : 영문(a-z, A-z), 숫자(0-9), 밑줄(_), 마침표(.) + - 금지된 문자: 공백, 특수문자, 이모지, 연속된마침표, 마침표로 시작하거나 끝나는것 + - 대소문자 구분없음 -> 중복 방지를 위해 모두 소문자로 변환하여 저장함 + - 유저이름은 고유 + - 특정 문자 단어제한 : discord, admin, moderator, 욕설 + - 비밀번호: 필수, 개수 제한 5미만 x + - 생년월일: 필수, 13세 이상만 사용가능 \ No newline at end of file From 004294a7c7ed8577e476114b4fdd1c1af8f3df9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:51:03 +0900 Subject: [PATCH 05/38] docs: change branch for push remote my all branch --- .github/workflows/sprint-mission1-pr-test.yml | 2 +- .../domain/user/validation/UsernameValidatorTest.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/sprint-mission1-pr-test.yml b/.github/workflows/sprint-mission1-pr-test.yml index 893253cb7..1799da75f 100644 --- a/.github/workflows/sprint-mission1-pr-test.yml +++ b/.github/workflows/sprint-mission1-pr-test.yml @@ -3,7 +3,7 @@ name: sprint mission 1 PR Test on: push: branches: - - 'part1-백재우-sprint1' + - '*' pull_request: branches: - 'part1-백재우' diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java index 503782dd9..37729f2c9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java @@ -1,11 +1,8 @@ package com.sprint.mission.discodeit.domain.user.validation; -import static org.junit.jupiter.api.Assertions.*; - import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; import java.util.stream.Stream; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; From 1b1848b6314496b5a2251091f84d9428c4077ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:40:57 +0900 Subject: [PATCH 06/38] =?UTF-8?q?feat:=20=EC=83=9D=EB=85=84=EC=9B=94?= =?UTF-8?q?=EC=9D=BC=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 생각해 볼 부분, 현재 static 으로 정적메서드가 많이 존재하는데, 검증해야하는 부분들이 많아질 경우 어떻게 할 것인가? 장단점 생각해보기 검증하는 로직을 외부 객체로 진행하게 된다면, 장단점 생각해보기 --- .../discodeit/domain/user/BirthDate.java | 13 ++++-- .../mission/discodeit/domain/user/Email.java | 2 +- .../discodeit/domain/user/Nickname.java | 2 +- .../discodeit/domain/user/Password.java | 10 +++-- .../discodeit/domain/user/Username.java | 2 +- .../exception/BirthDateInvalidException.java | 12 ++++++ .../user/validation/BirthDateValidator.java | 21 +++++++++ .../user/validation/EmailValidator.java | 5 ++- .../user/validation/NicknameValidator.java | 13 +++++- .../user/validation/PasswordValidator.java | 27 ++++++++++++ .../user/validation/UsernameValidator.java | 2 +- .../discodeit/global/error/ErrorCode.java | 2 + .../discodeit/domain/user/PasswordTest.java | 35 +++++++++++++++ .../discodeit/domain/user/UsernameTest.java | 2 +- .../validation/BirthDateValidatorTest.java | 18 ++++++++ .../validation/NicknameValidatorTest.java | 1 + .../validation/PasswordValidatorTest.java | 43 +++++++++++++++++++ .../validation/UsernameValidatorTest.java | 2 +- ...0 \353\252\250\353\215\270\353\247\201.md" | 2 +- 19 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/BirthDateInvalidException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/PasswordTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java index ff4f2883f..411cec7b1 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java @@ -1,16 +1,21 @@ package com.sprint.mission.discodeit.domain.user; +import com.sprint.mission.discodeit.domain.user.validation.BirthDateValidator; import java.time.LocalDate; +import lombok.Getter; +@Getter public class BirthDate { private final LocalDate value; - public BirthDate(int year, int month, int day) { - this.value = LocalDate.of(year, month, day); + public BirthDate(LocalDate value) { + BirthDateValidator.validate(value); + this.value = value; } - public LocalDate getValue() { - return value; + public static BirthDate of(int year, int month, int day) { + return new BirthDate(LocalDate.of(year, month, day)); } + } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java index 7ec6f463b..f8d1cdbe3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java @@ -14,6 +14,6 @@ public class Email { public Email(String email) { EmailValidator.valid(email); - this.value = email.trim(); + this.value = email; } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java index 283f2cb4d..9b488f3dd 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java @@ -14,7 +14,7 @@ public class Nickname { public Nickname(String value) { NicknameValidator.validate(value); - this.value = value.trim(); + this.value = value; } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java index 8eb906f79..9ece7c363 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java @@ -1,14 +1,18 @@ package com.sprint.mission.discodeit.domain.user; +import com.sprint.mission.discodeit.domain.user.validation.PasswordValidator; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString(of = "value") public class Password { private final String value; public Password(String value) { + PasswordValidator.validate(value); this.value = value; } - public String getValue() { - return value; - } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java index f4b7d4da1..5ca240fc7 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java @@ -14,7 +14,7 @@ public class Username { public Username(String value) { UsernameValidator.validate(value); - this.value = value.trim(); + this.value = value.toLowerCase(); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/BirthDateInvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/BirthDateInvalidException.java new file mode 100644 index 000000000..423e9dcac --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/BirthDateInvalidException.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class BirthDateInvalidException extends InvalidException { + + public BirthDateInvalidException(ErrorCode errorCode, String message) { + super(errorCode, message); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java new file mode 100644 index 000000000..cf8469d30 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java @@ -0,0 +1,21 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import com.sprint.mission.discodeit.domain.user.exception.BirthDateInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.time.LocalDate; +import java.util.Objects; + +public class BirthDateValidator { + private static final int MIN_AGE_RESTRICT = 13; + + public static void validate(LocalDate birthDate) { + if (Objects.isNull(birthDate)) { + throw new BirthDateInvalidException(ErrorCode.BIRTHDATE_REQUIRED, ""); + } + + LocalDate minimumAllowedYear = LocalDate.now().minusYears(MIN_AGE_RESTRICT); + if (!birthDate.isBefore(minimumAllowedYear)) { + throw new BirthDateInvalidException(ErrorCode.UNDERAGE_SIGNUP_REGISTERED, birthDate.toString()); + } + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java index 22549af1d..53844f0e6 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java @@ -14,10 +14,11 @@ public class EmailValidator { public static void valid(String email) { - if (Objects.isNull(email)) { + if (Objects.isNull(email) || email.isBlank()) { throw new EmailInvalidException(ErrorCode.EMAIL_REQUIRED, ""); } - if (email.isBlank() || !EMAIL_PATTERN.matcher(email).matches()) { + + if (!EMAIL_PATTERN.matcher(email).matches()) { throw new EmailInvalidException(ErrorCode.INVALID_EMAIL_FORMAT, email); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java index c741fc717..3a39a8a4f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java @@ -4,6 +4,7 @@ import com.sprint.mission.discodeit.global.error.ErrorCode; import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; public class NicknameValidator { private static final int MAX_LENGTH = 32; @@ -12,15 +13,23 @@ public class NicknameValidator { "운영자", "관리자", "봇" ); + private static final String VALID_NICK_NAME_REGEX = + "^(?!.*\\s)[\\p{L}\\p{N}\\p{P}\\p{S}\\p{So}]{1,32}$\n"; + private static final Pattern VALID_NICK_NAME_PATTERN = Pattern.compile(VALID_NICK_NAME_REGEX); + public static void validate(String value) { - if (Objects.isNull(value)) { + if (Objects.isNull(value) || value.isBlank()) { throw new NickNameInvalidException(ErrorCode.NICKNAME_REQUIRED, ""); } - if (value.isBlank() || value.length() > MAX_LENGTH) { + if (value.length() > MAX_LENGTH) { throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_LENGTH, value); } + if (!VALID_NICK_NAME_PATTERN.matcher(value).matches()) { + throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_FORMAT, value); + } + String lowerCase = value.toLowerCase(); for (String word : FORBIDDEN_WORDS) { if (lowerCase.contains(word)) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java new file mode 100644 index 000000000..64af5213f --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java @@ -0,0 +1,27 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import com.sprint.mission.discodeit.domain.user.exception.PassWordInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; +import java.util.regex.Pattern; + +public class PasswordValidator { + private final static int MAX_PASSWORD_LENGTH = 100; + private final static int MIN_PASSWORD_LENGTH = 8; + private final static String PASSWORD_REX = "^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?=.*[!@#$%^&*()_+])\\S{8,}$"; + private final static Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REX); + + public static void validate(String password) { + if (Objects.isNull(password) || password.isEmpty()) { + throw new PassWordInvalidException(ErrorCode.PASSWORD_REQUIRED, ""); + } + + if (password.length() < MIN_PASSWORD_LENGTH || password.length() > MAX_PASSWORD_LENGTH) { + throw new PassWordInvalidException(ErrorCode.INVALID_PASSWORD_LENGTH, password); + } + + if (!PASSWORD_PATTERN.matcher(password).matches()) { + throw new PassWordInvalidException(ErrorCode.WEAK_PASSWORD, password); + } + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java index e373bffdd..679a8b9b0 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java @@ -20,7 +20,7 @@ public class UsernameValidator { public static void validate(final String username) { - if (Objects.isNull(username)) { + if (Objects.isNull(username) || username.isBlank()) { throw new UserNameInvalidException(ErrorCode.USERNAME_REQUIRED, ""); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index 250ab4e6a..e703ecb7b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -8,11 +8,13 @@ public enum ErrorCode { INVALID_NICKNAME_LENGTH(400, "유저 이름은 2~32자 이내여야 합니다.", "U002"), INVALID_PASSWORD_LENGTH(400, "비밀번호는 8~20자 이내여야 합니다.", "U004"), // 비밀번호 길이 오류 WEAK_PASSWORD(400, "비밀번호는 대문자, 숫자, 특수문자를 포함해야 합니다.", "U008"), + UNDERAGE_SIGNUP_REGISTERED(400, "13세 이상만 회원가입이 가능합니다.", "U009"), USERNAME_REQUIRED(400, "유저 이름은 필수입니다.", "U104"), NICKNAME_REQUIRED(400, "닉네임은 필수입니다.", "U105"), EMAIL_REQUIRED(400, "이메일은 필수입니다.", "U106"), PASSWORD_REQUIRED(400, "비밀번호는 필수 입력값입니다.", "U107"), + BIRTHDATE_REQUIRED(400, "생년월일은 필수 입력값입니다.", "U108"), INVALID_EMAIL_FORMAT(400, "이메일 형식이 올바르지 않습니다", "U203"), INVALID_USERNAME_FORMAT(400, "유저 이름에 허용되지 않은 문자가 포함되어 있습니다.", "U205"), diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/PasswordTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/PasswordTest.java new file mode 100644 index 000000000..56c902d3a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/PasswordTest.java @@ -0,0 +1,35 @@ +package com.sprint.mission.discodeit.domain.user; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PasswordTest { + + @ParameterizedTest + @ValueSource(strings = { + "A1b@5678", // 최소 길이(8) 만족 + "Secure#99", // 대문자+소문자+숫자+특수문자 포함 + "StrongP@ssw0rd", // 일반적인 강력한 비밀번호 + "Aa1!Aa1!Aa1!", // 패턴 반복 + "P@55w0rD2024", // 숫자 포함된 조합 + "Valid123!@#", // 숫자 + 특수문자 여러 개 포함 + "Pass1234!", // 일반적인 패턴 + "LONGpassword123$LongPassword", // 대문자, 소문자, 숫자, 특수문자 포함 (길이 32자) + "Xx1!yY2@zZ3#", // 랜덤한 조합 + "C0mpl!c@tedPwd99$", // 다양한 조합 + "ABCDEFGh1@", // 대문자 여러 개, 소문자 하나 포함 + "abcdEFG123$", // 대소문자, 숫자, 특수문자 조합 + "1Password!", // 일반적인 패턴 + "P@ssw0rddSecure123!", // 긴 길이 (최대 100자는 넘지 않음) + }) + void 비밀번호_생성_성공(String password) { + //given + // when + Password createdPassword = new Password(password); + // then + assertThat(createdPassword.getValue()).isEqualTo(password); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java index ce1708b01..90ce0115b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java @@ -24,7 +24,7 @@ class UsernameTest { // when Username createUsername = new Username(username); // then - assertThat(createUsername.getValue()).isEqualTo(username); + assertThat(createUsername.getValue()).isEqualToIgnoringCase(username); } } \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java new file mode 100644 index 000000000..6242d8416 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java @@ -0,0 +1,18 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class BirthDateValidatorTest { + + @Test + void 나이_13세_미만_생성검증_에러throw() { + //given + + // when + + // then + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java index 3b7bdad9d..afbd5b4a6 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java @@ -26,6 +26,7 @@ class NicknameValidatorTest { "hello admin!", // 금지된 단어 포함 "AdminUser", // 금지된 단어 포함 (대소문자 무시) "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) + "papago good", "root_user", // 금지된 단어 포함 (관리자 권한 관련) "system_mod", // 금지된 단어 포함 "운영자", // 한글 운영자 금지 diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java new file mode 100644 index 000000000..b7cbcdd37 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java @@ -0,0 +1,43 @@ +package com.sprint.mission.discodeit.domain.user.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import com.sprint.mission.discodeit.domain.user.exception.PassWordInvalidException; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class PasswordValidatorTest { + + @ParameterizedTest + @NullAndEmptySource + @MethodSource("passwordOverLengthProvider") + @ValueSource(strings = { + "Ab1!", // 8자 미만 + "password1!", // 대문자 없음 + "PASSWORD1!", // 소문자 없음 + "Password!", // 숫자 없음 + "Password1", // 특수문자 없음 + "Password 1!", // 공백 포함 + "Password1한", // 한글 포함 + "12345678!", // 숫자 + 특수문자만 (영문 없음) + "ABCDEFGH1!", // 대문자 + 숫자 + 특수문자만 (소문자 없음) + "abcdefgh1!", // 소문자 + 숫자 + 특수문자만 (대문자 없음) + "Password blanck", + }) + void 비밀번호_정규식_생성_검증_에러throw(String password) { + //given + // when + Throwable catchThrow = catchThrowable(() -> PasswordValidator.validate(password)); + // then + assertThat(catchThrow).isInstanceOf(PassWordInvalidException.class); + } + + static Stream passwordOverLengthProvider() { + return Stream.of("A1!a".repeat(26)); + } + +} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java index 37729f2c9..975c06466 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java @@ -36,7 +36,7 @@ class UsernameValidatorTest { "봇", // 봇 계정 금지 }) @MethodSource("usernameOverLengthProvider") - void testMethodNameHere(String username) { + void 유저_이름_제한_검증_에러throw(String username) { //given // when Throwable catchThrowable = Assertions.catchThrowable(() -> UsernameValidator.validate(username)); diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 20505769a..b1a4833b9 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -20,5 +20,5 @@ - 대소문자 구분없음 -> 중복 방지를 위해 모두 소문자로 변환하여 저장함 - 유저이름은 고유 - 특정 문자 단어제한 : discord, admin, moderator, 욕설 - - 비밀번호: 필수, 개수 제한 5미만 x + - 비밀번호: 필수, 개수 제한 8미만 x , 영문 대문자, 소문자, 숫자, 특수문자가 들어가야합니다. 공백 x - 생년월일: 필수, 13세 이상만 사용가능 \ No newline at end of file From 59261adefb9cae90e2b253eaf64875b499723b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Fri, 7 Feb 2025 20:53:32 +0900 Subject: [PATCH 07/38] =?UTF-8?q?refactor=20:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B0=9C=ED=96=89=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discodeit/domain/user/validation/NicknameValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java index 3a39a8a4f..bdc8b656d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java @@ -14,7 +14,7 @@ public class NicknameValidator { ); private static final String VALID_NICK_NAME_REGEX = - "^(?!.*\\s)[\\p{L}\\p{N}\\p{P}\\p{S}\\p{So}]{1,32}$\n"; + "^(?!.*\\s)[\\p{L}\\p{N}\\p{P}\\p{S}\\p{So}]{1,32}$"; private static final Pattern VALID_NICK_NAME_PATTERN = Pattern.compile(VALID_NICK_NAME_REGEX); public static void validate(String value) { From cb2ba91ee83dec1b539432d6d4ec58b831b7f303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Fri, 7 Feb 2025 22:04:04 +0900 Subject: [PATCH 08/38] =?UTF-8?q?feat:=20user=20join=20feature=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 --- .../application/dto/JoinUserReqeustDto.java | 16 +++++++ .../application/dto/UserResponseDto.java | 9 ++++ .../application/service/UserService.java | 33 ++++++++++++++ .../service/converter/UserConverter.java | 31 +++++++++++++ .../mission/discodeit/domain/user/User.java | 44 +++++++------------ .../user/InMemoryUserRepository.java | 30 +++++++++++++ .../user/interfaces/UserRepository.java | 11 +++++ ...0 \353\252\250\353\215\270\353\247\201.md" | 3 +- 8 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java new file mode 100644 index 000000000..b6cbf36c8 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java @@ -0,0 +1,16 @@ +package com.sprint.mission.discodeit.application.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +public record JoinUserReqeustDto( + @NotBlank String nickname, + @NotBlank String username, + @Email String email, + @NotBlank String password, + @NotNull LocalDate birthDate +) { + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java new file mode 100644 index 000000000..dbe8eb232 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.application.dto; + +public record UserResponseDto( + String nickname, + String username, + String email +) { + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java new file mode 100644 index 000000000..fd6f05b2e --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java @@ -0,0 +1,33 @@ +package com.sprint.mission.discodeit.application.service; + +import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.UserResponseDto; +import com.sprint.mission.discodeit.application.service.converter.UserConverter; +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + private final UserRepository userRepository; + private final UserConverter userConverter; + + public UserService( + UserRepository userRepository, + UserConverter userConverter + ) { + this.userRepository = userRepository; + this.userConverter = userConverter; + } + + public UserResponseDto join(JoinUserReqeustDto requestDto) { + boolean isExist = userRepository.isExistByEmail(new Email(requestDto.email())); + if (isExist) { + throw new IllegalArgumentException(); + } + User savedUser = userRepository.save(userConverter.toUser(requestDto)); + return userConverter.toDto(savedUser); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java new file mode 100644 index 000000000..06d4671b2 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java @@ -0,0 +1,31 @@ +package com.sprint.mission.discodeit.application.service.converter; + +import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.UserResponseDto; +import com.sprint.mission.discodeit.domain.user.BirthDate; +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.Nickname; +import com.sprint.mission.discodeit.domain.user.Password; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; +import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; +import org.springframework.stereotype.Component; + +@Component +public class UserConverter { + + public User toUser(JoinUserReqeustDto requestDto) { + return User.builder() + .username(new Username(requestDto.username())) + .nickname(new Nickname(requestDto.nickname())) + .email(new Email(requestDto.email())) + .password(new Password(requestDto.password())) + .birthDate(new BirthDate(requestDto.birthDate())) + .emailSubscriptionStatus(EmailSubscriptionStatus.UNSUBSCRIBED) // TODO 이넘 타입 dto에서 어떻게 받아서 넘겨야하나. + .build(); + } + + public UserResponseDto toDto(User user) { + return new UserResponseDto(user.getNicknameValue(),user.getUsernameValue(),user.getEmailValue()); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java index 0d07babfc..161b06e8e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -4,12 +4,13 @@ import java.time.LocalDateTime; import java.util.Objects; import java.util.UUID; +import lombok.Builder; public class User { private final UUID id; private final Nickname nickname; - private final Nickname username; + private final Username username; private final Email email; private final Password password; private final BirthDate birthDate; @@ -17,13 +18,14 @@ public class User { private final LocalDateTime updatedAt; private final EmailSubscriptionStatus emailSubscriptionStatus; + @Builder public User( - Nickname nickname, - Nickname username, - Email email, - Password password, - BirthDate birthDate, - EmailSubscriptionStatus emailSubscriptionStatus + Nickname nickname, + Username username, + Email email, + Password password, + BirthDate birthDate, + EmailSubscriptionStatus emailSubscriptionStatus ) { this.id = UUID.randomUUID(); this.createdAt = LocalDateTime.now(); @@ -40,36 +42,20 @@ public UUID getId() { return id; } - public Nickname getNickname() { - return nickname; - } - public Email getEmail() { return email; } - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public Nickname getUsername() { - return username; - } - - public Password getPassword() { - return password; + public String getUsernameValue() { + return this.username.getValue(); } - public BirthDate getBirthDate() { - return birthDate; + public String getNicknameValue() { + return this.nickname.getValue(); } - public EmailSubscriptionStatus getEmailSubscriptionStatus() { - return emailSubscriptionStatus; + public String getEmailValue() { + return this.email.getValue(); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java new file mode 100644 index 000000000..45b41edf7 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -0,0 +1,30 @@ +package com.sprint.mission.discodeit.repository.user; + +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.springframework.stereotype.Repository; + +@Repository +public class InMemoryUserRepository implements UserRepository { + + Map store = new HashMap<>(); + Set emails = new HashSet<>(); + + @Override + public User save(User user) { + store.put(user.getId(),user); + emails.add(user.getEmail()); + return user; + } + + @Override + public boolean isExistByEmail(Email email) { + return emails.contains(email); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java new file mode 100644 index 000000000..b5f9af39a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.repository.user.interfaces; + +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.User; + +public interface UserRepository { + + User save(User user); + + boolean isExistByEmail(Email email); +} diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index b1a4833b9..79f42a541 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -21,4 +21,5 @@ - 유저이름은 고유 - 특정 문자 단어제한 : discord, admin, moderator, 욕설 - 비밀번호: 필수, 개수 제한 8미만 x , 영문 대문자, 소문자, 숫자, 특수문자가 들어가야합니다. 공백 x - - 생년월일: 필수, 13세 이상만 사용가능 \ No newline at end of file + - 생년월일: 필수, 13세 이상만 사용가능 + - 사용하고 있는 이메일인지 검증한다. From 4c077b0133bce9910c1903b79487669ffa852184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Fri, 7 Feb 2025 23:59:23 +0900 Subject: [PATCH 09/38] =?UTF-8?q?test:=20user=20service,=20repository=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/UserService.java | 15 ++++-- .../mission/discodeit/domain/user/User.java | 4 ++ .../exception/AlreadyUserExistsException.java | 11 +++++ .../user/InMemoryUserRepository.java | 9 +++- .../user/interfaces/UserRepository.java | 3 ++ .../application/service/UserServiceTest.java | 46 +++++++++++++++++++ .../mission/discodeit/fake/FakeFactory.java | 19 ++++++++ .../discodeit/fake/domain/user/StubUser.java | 43 +++++++++++++++++ .../fake/repository/FakeUserRepository.java | 36 +++++++++++++++ .../user/interfaces/UserRepositoryTest.java | 27 +++++++++++ 10 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/AlreadyUserExistsException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepositoryTest.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java index fd6f05b2e..d49aa1f7f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java @@ -5,6 +5,9 @@ import com.sprint.mission.discodeit.application.service.converter.UserConverter; import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; +import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; +import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import org.springframework.stereotype.Service; @@ -23,10 +26,16 @@ public UserService( } public UserResponseDto join(JoinUserReqeustDto requestDto) { - boolean isExist = userRepository.isExistByEmail(new Email(requestDto.email())); - if (isExist) { - throw new IllegalArgumentException(); + boolean isEmailAlreadyUsed = userRepository.isExistByEmail(new Email(requestDto.email())); + if (isEmailAlreadyUsed) { + throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, requestDto.email()); } + + boolean isUsernameAlreadyUsed = userRepository.isExistByUsername(new Username(requestDto.username())); + if (isUsernameAlreadyUsed) { + throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_USERNAME, requestDto.username()); + } + User savedUser = userRepository.save(userConverter.toUser(requestDto)); return userConverter.toDto(savedUser); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java index 161b06e8e..be604a7be 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -42,6 +42,10 @@ public UUID getId() { return id; } + public Username getUsername() { + return username; + } + public Email getEmail() { return email; } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/AlreadyUserExistsException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/AlreadyUserExistsException.java new file mode 100644 index 000000000..11d27f695 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/AlreadyUserExistsException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class AlreadyUserExistsException extends InvalidException { + + public AlreadyUserExistsException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java index 45b41edf7..3fa60e569 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; import java.util.HashSet; @@ -15,10 +16,11 @@ public class InMemoryUserRepository implements UserRepository { Map store = new HashMap<>(); Set emails = new HashSet<>(); + Set usernames = new HashSet<>(); @Override public User save(User user) { - store.put(user.getId(),user); + store.put(user.getId(), user); emails.add(user.getEmail()); return user; } @@ -27,4 +29,9 @@ public User save(User user) { public boolean isExistByEmail(Email email) { return emails.contains(email); } + + @Override + public boolean isExistByUsername(Username username) { + return usernames.contains(username); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java index b5f9af39a..a345c2425 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java @@ -2,10 +2,13 @@ import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; public interface UserRepository { User save(User user); boolean isExistByEmail(Email email); + + boolean isExistByUsername(Username username); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java new file mode 100644 index 000000000..532a72f20 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java @@ -0,0 +1,46 @@ +package com.sprint.mission.discodeit.application.service; + +import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; +import com.sprint.mission.discodeit.fake.FakeFactory; +import com.sprint.mission.discodeit.fake.domain.user.StubUser; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class UserServiceTest { + + private UserService userService; + private JoinUserReqeustDto joinUserReqeustDto = StubUser.generateJoinRequestDto(); + + @BeforeEach + void setup() { + userService = FakeFactory.getUserService(); + } + + @Test + void 회원가입_요청_메소드_호출_이미_존재하는_이메일_이면_에러throw() { + // given + userService.join(joinUserReqeustDto); + JoinUserReqeustDto anotherUsernameRequest = StubUser.generateJoinRequestByUsername("anotherusername"); + + // when + Throwable catchThrow = Assertions.catchThrowable( + () -> userService.join(anotherUsernameRequest)); + // then + Assertions.assertThat(catchThrow).isInstanceOf(AlreadyUserExistsException.class); + } + + @Test + void 회원가입_요청_이미_존재하는_유저이름_요청_에러throw() { + // given + userService.join(joinUserReqeustDto); + JoinUserReqeustDto anotherEmailRequest = StubUser.generateJoinRequestByEmail("another@test.com"); + + // when + Throwable catchThrow = Assertions.catchThrowable( + () -> userService.join(anotherEmailRequest)); + // then + Assertions.assertThat(catchThrow).isInstanceOf(AlreadyUserExistsException.class); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java new file mode 100644 index 000000000..b2e19b985 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java @@ -0,0 +1,19 @@ +package com.sprint.mission.discodeit.fake; + +import com.sprint.mission.discodeit.application.service.UserService; +import com.sprint.mission.discodeit.application.service.converter.UserConverter; +import com.sprint.mission.discodeit.fake.repository.FakeUserRepository; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; + +public class FakeFactory { + private FakeFactory() { + } + + public static UserRepository getUserRepository() { + return new FakeUserRepository(); + } + + public static UserService getUserService() { + return new UserService(getUserRepository(), new UserConverter()); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java new file mode 100644 index 000000000..3926dd5c2 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java @@ -0,0 +1,43 @@ +package com.sprint.mission.discodeit.fake.domain.user; + +import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.domain.user.BirthDate; +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.Nickname; +import com.sprint.mission.discodeit.domain.user.Password; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; +import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; +import java.time.LocalDate; + +public class StubUser { + + private static final String USER_NAME = "user123"; + private static final String NICK_NAME = "User1"; + private static final String EMAIL = "test@example.com"; + private static final String PASSWORD = "A1b@5678"; + private static final LocalDate BIRTH_DATE = LocalDate.of(2000, 1, 1); + + public static User generateUser() { + return User.builder() + .username(new Username(USER_NAME)) + .nickname(new Nickname(NICK_NAME)) + .email(new Email(EMAIL)) + .password(new Password(PASSWORD)) + .birthDate(new BirthDate(BIRTH_DATE)) + .emailSubscriptionStatus(EmailSubscriptionStatus.SUBSCRIBED) + .build(); + } + + public static JoinUserReqeustDto generateJoinRequestDto() { + return new JoinUserReqeustDto(NICK_NAME, USER_NAME, EMAIL, PASSWORD, BIRTH_DATE); + } + + public static JoinUserReqeustDto generateJoinRequestByUsername(String username) { + return new JoinUserReqeustDto(NICK_NAME, username, EMAIL, PASSWORD, BIRTH_DATE); + } + + public static JoinUserReqeustDto generateJoinRequestByEmail(String email) { + return new JoinUserReqeustDto(NICK_NAME, USER_NAME, email, PASSWORD, BIRTH_DATE); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java new file mode 100644 index 000000000..f13964d13 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java @@ -0,0 +1,36 @@ +package com.sprint.mission.discodeit.fake.repository; + +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class FakeUserRepository implements UserRepository { + + Map store = new HashMap<>(); + Set emails = new HashSet<>(); + Set usernames = new HashSet<>(); + + @Override + public User save(User user) { + store.put(user.getId(), user); + emails.add(user.getEmail()); + usernames.add(user.getUsername()); + return user; + } + + @Override + public boolean isExistByEmail(Email email) { + return emails.contains(email); + } + + @Override + public boolean isExistByUsername(Username username) { + return usernames.contains(username); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepositoryTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepositoryTest.java new file mode 100644 index 000000000..c8018a376 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepositoryTest.java @@ -0,0 +1,27 @@ +package com.sprint.mission.discodeit.repository.user.interfaces; + +import static org.junit.jupiter.api.Assertions.*; + +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.fake.FakeFactory; +import com.sprint.mission.discodeit.fake.domain.user.StubUser; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class UserRepositoryTest { + + private final UserRepository userRepository = FakeFactory.getUserRepository(); + private final User user = StubUser.generateUser(); + + @Test + void 사용중인_이메일_검증() { + // given + userRepository.save(user); + String existedEmailValue = user.getEmailValue(); + // when + boolean result = userRepository.isExistByEmail(new Email(existedEmailValue)); + // then + Assertions.assertThat(result).isTrue(); + } +} From 904b25288b01a7c914f3c27f0c67f3b36d6b1e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sat, 8 Feb 2025 01:36:43 +0900 Subject: [PATCH 10/38] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=95=94=ED=98=B8=ED=99=94=20?= =?UTF-8?q?=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 --- .../1-sprint-mission/build.gradle | 2 + .../application/auth/PasswordEncoder.java | 17 +++++ .../application/service/UserService.java | 42 ------------ .../application/service/user/UserService.java | 67 +++++++++++++++++++ .../{ => user}/converter/UserConverter.java | 2 +- .../discodeit/domain/user/Password.java | 17 +++-- .../user/validation/PasswordValidator.java | 2 +- .../application/auth/PasswordEncoderTest.java | 21 ++++++ .../application/service/UserServiceTest.java | 1 + .../validation/PasswordValidatorTest.java | 4 +- .../mission/discodeit/fake/FakeFactory.java | 7 +- ...0 \353\252\250\353\215\270\353\247\201.md" | 10 +++ 12 files changed, 138 insertions(+), 54 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/auth/PasswordEncoder.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/{ => user}/converter/UserConverter.java (94%) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/auth/PasswordEncoderTest.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/build.gradle b/codeit-bootcamp-spring/1-sprint-mission/build.gradle index f8c023fa7..6feb1dcd3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/build.gradle +++ b/codeit-bootcamp-spring/1-sprint-mission/build.gradle @@ -33,6 +33,8 @@ dependencies { // https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' + // https://mvnrepository.com/artifact/org.mindrot/jbcrypt + implementation 'org.mindrot:jbcrypt:0.4' } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/auth/PasswordEncoder.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/auth/PasswordEncoder.java new file mode 100644 index 000000000..dcbc7429e --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/auth/PasswordEncoder.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.application.auth; + +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.stereotype.Component; + +@Component +public class PasswordEncoder { + + public String encode(String rawPassword) { + return BCrypt.hashpw(rawPassword, BCrypt.gensalt(12)); + } + + public boolean matches(String rawPassword, String encodedPassword) { + return BCrypt.checkpw(rawPassword, encodedPassword); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java deleted file mode 100644 index d49aa1f7f..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/UserService.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sprint.mission.discodeit.application.service; - -import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; -import com.sprint.mission.discodeit.application.dto.UserResponseDto; -import com.sprint.mission.discodeit.application.service.converter.UserConverter; -import com.sprint.mission.discodeit.domain.user.Email; -import com.sprint.mission.discodeit.domain.user.User; -import com.sprint.mission.discodeit.domain.user.Username; -import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; -import com.sprint.mission.discodeit.global.error.ErrorCode; -import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; -import org.springframework.stereotype.Service; - -@Service -public class UserService { - - private final UserRepository userRepository; - private final UserConverter userConverter; - - public UserService( - UserRepository userRepository, - UserConverter userConverter - ) { - this.userRepository = userRepository; - this.userConverter = userConverter; - } - - public UserResponseDto join(JoinUserReqeustDto requestDto) { - boolean isEmailAlreadyUsed = userRepository.isExistByEmail(new Email(requestDto.email())); - if (isEmailAlreadyUsed) { - throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, requestDto.email()); - } - - boolean isUsernameAlreadyUsed = userRepository.isExistByUsername(new Username(requestDto.username())); - if (isUsernameAlreadyUsed) { - throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_USERNAME, requestDto.username()); - } - - User savedUser = userRepository.save(userConverter.toUser(requestDto)); - return userConverter.toDto(savedUser); - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java new file mode 100644 index 000000000..e0a6ebf52 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java @@ -0,0 +1,67 @@ +package com.sprint.mission.discodeit.application.service.user; + +import com.sprint.mission.discodeit.application.auth.PasswordEncoder; +import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.UserResponseDto; +import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; +import com.sprint.mission.discodeit.domain.user.BirthDate; +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.Nickname; +import com.sprint.mission.discodeit.domain.user.Password; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; +import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; +import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; +import com.sprint.mission.discodeit.domain.user.validation.PasswordValidator; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + private final UserRepository userRepository; + private final UserConverter userConverter; + private final PasswordEncoder passwordEncoder; + + public UserService( + UserRepository userRepository, + UserConverter userConverter, + PasswordEncoder passwordEncoder + ) { + this.userRepository = userRepository; + this.userConverter = userConverter; + this.passwordEncoder = passwordEncoder; + } + + public UserResponseDto join(JoinUserReqeustDto requestDto) { + checkEmailAlreadyUsedOrThrow(requestDto.email()); + checkUsernameAlreadyUsedOrThrow(requestDto.username()); + PasswordValidator.validateOrThrow(requestDto.password()); + User savedUser = userRepository.save(toUserWithPasswordEncode(requestDto)); + return userConverter.toDto(savedUser); + } + + private void checkEmailAlreadyUsedOrThrow(String email) { + if (userRepository.isExistByEmail(new Email(email))) { + throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, email); + } + } + + private void checkUsernameAlreadyUsedOrThrow(String username) { + if (userRepository.isExistByUsername(new Username(username))) { + throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_USERNAME, username); + } + } + + private User toUserWithPasswordEncode(JoinUserReqeustDto requestDto) { + return User.builder() + .username(new Username(requestDto.username())) + .nickname(new Nickname(requestDto.nickname())) + .email(new Email(requestDto.email())) + .password(new Password(passwordEncoder.encode(requestDto.password()))) + .birthDate(new BirthDate(requestDto.birthDate())) + .emailSubscriptionStatus(EmailSubscriptionStatus.UNSUBSCRIBED) + .build(); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java similarity index 94% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java index 06d4671b2..cd6514c8c 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/converter/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java @@ -1,4 +1,4 @@ -package com.sprint.mission.discodeit.application.service.converter; +package com.sprint.mission.discodeit.application.service.user.converter; import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; import com.sprint.mission.discodeit.application.dto.UserResponseDto; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java index 9ece7c363..e9362775d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java @@ -1,18 +1,25 @@ package com.sprint.mission.discodeit.domain.user; -import com.sprint.mission.discodeit.domain.user.validation.PasswordValidator; -import lombok.Getter; import lombok.ToString; -@Getter @ToString(of = "value") public class Password { - private final String value; + private String value; public Password(String value) { - PasswordValidator.validate(value); this.value = value; } + public void changePassword(String encodedPassword) { + setValue(encodedPassword); + } + + private void setValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java index 64af5213f..9a2bcfe2a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java @@ -11,7 +11,7 @@ public class PasswordValidator { private final static String PASSWORD_REX = "^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?=.*[!@#$%^&*()_+])\\S{8,}$"; private final static Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_REX); - public static void validate(String password) { + public static void validateOrThrow(String password) { if (Objects.isNull(password) || password.isEmpty()) { throw new PassWordInvalidException(ErrorCode.PASSWORD_REQUIRED, ""); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/auth/PasswordEncoderTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/auth/PasswordEncoderTest.java new file mode 100644 index 000000000..143ac2048 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/auth/PasswordEncoderTest.java @@ -0,0 +1,21 @@ +package com.sprint.mission.discodeit.application.auth; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class PasswordEncoderTest { + + @Test + void 암호화_한_비밀번호_검증() { + // given + String password = "testPassword"; + PasswordEncoder encoder = new PasswordEncoder(); + // when + String encodedPassword = encoder.encode(password); + // then + assertTrue(encoder.matches(password, encodedPassword)); + assertFalse(encoder.matches("anotherPassword",encodedPassword)); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java index 532a72f20..9ad8465df 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.application.service; import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.service.user.UserService; import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; import com.sprint.mission.discodeit.fake.FakeFactory; import com.sprint.mission.discodeit.fake.domain.user.StubUser; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java index b7cbcdd37..ce87bb88b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidatorTest.java @@ -31,7 +31,7 @@ class PasswordValidatorTest { void 비밀번호_정규식_생성_검증_에러throw(String password) { //given // when - Throwable catchThrow = catchThrowable(() -> PasswordValidator.validate(password)); + Throwable catchThrow = catchThrowable(() -> PasswordValidator.validateOrThrow(password)); // then assertThat(catchThrow).isInstanceOf(PassWordInvalidException.class); } @@ -40,4 +40,4 @@ static Stream passwordOverLengthProvider() { return Stream.of("A1!a".repeat(26)); } -} \ No newline at end of file +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java index b2e19b985..0a3f22b6f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java @@ -1,7 +1,8 @@ package com.sprint.mission.discodeit.fake; -import com.sprint.mission.discodeit.application.service.UserService; -import com.sprint.mission.discodeit.application.service.converter.UserConverter; +import com.sprint.mission.discodeit.application.auth.PasswordEncoder; +import com.sprint.mission.discodeit.application.service.user.UserService; +import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.fake.repository.FakeUserRepository; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; @@ -14,6 +15,6 @@ public static UserRepository getUserRepository() { } public static UserService getUserService() { - return new UserService(getUserRepository(), new UserConverter()); + return new UserService(getUserRepository(), new UserConverter(), new PasswordEncoder()); } } diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 79f42a541..63a5a431d 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -23,3 +23,13 @@ - 비밀번호: 필수, 개수 제한 8미만 x , 영문 대문자, 소문자, 숫자, 특수문자가 들어가야합니다. 공백 x - 생년월일: 필수, 13세 이상만 사용가능 - 사용하고 있는 이메일인지 검증한다. + - 사용하고 있는 유저이름인지 검증한다. + - 비밀번호를 저장할 때 암호화를 진행한다. + - [jBCrypt](https://www.mindrot.org/projects/jBCrypt/) + +- 로그인 + - `이메일`과 `비밀번호`를 입력받는다. + - 유저의 비밀번호가 틀리다면 에러를 반환한다. + + +-- newLine for git From 8994b9c35ec9f104acb6bd07c0dd834085fc6560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sat, 8 Feb 2025 01:56:06 +0900 Subject: [PATCH 11/38] =?UTF-8?q?refactor:=20In=20memory=20user=20reposito?= =?UTF-8?q?ry=20=ED=95=84=EB=93=9C=20=EC=9E=90=EB=A3=8C=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/InMemoryUserRepository.java | 17 ++++++++--------- .../fake/repository/FakeUserRepository.java | 18 ++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java index 3fa60e569..34151bac1 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -5,33 +5,32 @@ import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.UUID; import org.springframework.stereotype.Repository; @Repository public class InMemoryUserRepository implements UserRepository { - Map store = new HashMap<>(); - Set emails = new HashSet<>(); - Set usernames = new HashSet<>(); + Map uuidUsers = new HashMap<>(); + Map emailUsers = new HashMap<>(); + Map usernameUsers = new HashMap<>(); @Override public User save(User user) { - store.put(user.getId(), user); - emails.add(user.getEmail()); + uuidUsers.put(user.getId(), user); + emailUsers.put(user.getEmail(), user); + usernameUsers.put(user.getUsername(), user); return user; } @Override public boolean isExistByEmail(Email email) { - return emails.contains(email); + return emailUsers.containsKey(email); } @Override public boolean isExistByUsername(Username username) { - return usernames.contains(username); + return usernameUsers.containsKey(username); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java index f13964d13..86cfe9bc5 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java @@ -5,32 +5,30 @@ import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.UUID; public class FakeUserRepository implements UserRepository { - Map store = new HashMap<>(); - Set emails = new HashSet<>(); - Set usernames = new HashSet<>(); + Map uuidUsers = new HashMap<>(); + Map emailUsers = new HashMap<>(); + Map usernameUsers = new HashMap<>(); @Override public User save(User user) { - store.put(user.getId(), user); - emails.add(user.getEmail()); - usernames.add(user.getUsername()); + uuidUsers.put(user.getId(), user); + emailUsers.put(user.getEmail(), user); + usernameUsers.put(user.getUsername(), user); return user; } @Override public boolean isExistByEmail(Email email) { - return emails.contains(email); + return emailUsers.containsKey(email); } @Override public boolean isExistByUsername(Username username) { - return usernames.contains(username); + return usernameUsers.containsKey(username); } } From 1abd1ab5658c45067ae366d562f9b82f6a650a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sat, 8 Feb 2025 04:02:56 +0900 Subject: [PATCH 12/38] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=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 --- .../application/dto/LoginRequestDto.java | 9 +++++++ .../application/service/user/UserService.java | 27 ++++++++++++++++--- .../mission/discodeit/domain/user/User.java | 16 ++++++----- .../user/exception/InvalidLoginException.java | 10 +++++++ .../user/exception/UserNotFoundException.java | 10 +++++++ .../discodeit/global/error/ErrorCode.java | 4 ++- .../user/InMemoryUserRepository.java | 7 +++++ .../user/interfaces/UserRepository.java | 6 +++++ .../fake/repository/FakeUserRepository.java | 7 +++++ ...0 \353\252\250\353\215\270\353\247\201.md" | 2 ++ 10 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/InvalidLoginException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNotFoundException.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java new file mode 100644 index 000000000..64591d96d --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.application.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequestDto( + @NotBlank String email, + @NotBlank String password +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java index e0a6ebf52..ee3fc0bf3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.application.auth.PasswordEncoder; import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.LoginRequestDto; import com.sprint.mission.discodeit.application.dto.UserResponseDto; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.domain.user.BirthDate; @@ -12,9 +13,12 @@ import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; +import com.sprint.mission.discodeit.domain.user.exception.InvalidLoginException; +import com.sprint.mission.discodeit.domain.user.exception.UserNotFoundException; import com.sprint.mission.discodeit.domain.user.validation.PasswordValidator; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.util.UUID; import org.springframework.stereotype.Service; @Service @@ -25,9 +29,9 @@ public class UserService { private final PasswordEncoder passwordEncoder; public UserService( - UserRepository userRepository, - UserConverter userConverter, - PasswordEncoder passwordEncoder + UserRepository userRepository, + UserConverter userConverter, + PasswordEncoder passwordEncoder ) { this.userRepository = userRepository; this.userConverter = userConverter; @@ -42,6 +46,19 @@ public UserResponseDto join(JoinUserReqeustDto requestDto) { return userConverter.toDto(savedUser); } + public void login(LoginRequestDto requestDto) { + userRepository.findOneByEmail(new Email(requestDto.email())) + .filter(user -> matchUserPassword(requestDto.password(), user.getPasswordValue())) + .orElseThrow(() -> new InvalidLoginException(ErrorCode.INVALID_CREDENTIALS)); + // 3. 로그인 성공 시 로직 구현하기 - 쿠키(어떤 값 담지?), 세부적인건 우선순위를 뒤로 미루기 + // etc. 스프링 시큐리티 적용 시 리팩터링 최대한 변경에 유연하게 코딩 + } + + public User findOneByIdOrThrow(UUID uuid) { + return userRepository.findOneById(uuid) + .orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND)); + } + private void checkEmailAlreadyUsedOrThrow(String email) { if (userRepository.isExistByEmail(new Email(email))) { throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, email); @@ -54,6 +71,10 @@ private void checkUsernameAlreadyUsedOrThrow(String username) { } } + private boolean matchUserPassword(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + private User toUserWithPasswordEncode(JoinUserReqeustDto requestDto) { return User.builder() .username(new Username(requestDto.username())) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java index be604a7be..edf96a5bd 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -20,12 +20,12 @@ public class User { @Builder public User( - Nickname nickname, - Username username, - Email email, - Password password, - BirthDate birthDate, - EmailSubscriptionStatus emailSubscriptionStatus + Nickname nickname, + Username username, + Email email, + Password password, + BirthDate birthDate, + EmailSubscriptionStatus emailSubscriptionStatus ) { this.id = UUID.randomUUID(); this.createdAt = LocalDateTime.now(); @@ -62,6 +62,10 @@ public String getEmailValue() { return this.email.getValue(); } + public String getPasswordValue() { + return this.password.getValue(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/InvalidLoginException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/InvalidLoginException.java new file mode 100644 index 000000000..52aad5360 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/InvalidLoginException.java @@ -0,0 +1,10 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; + +public class InvalidLoginException extends EntityNotFoundException { + public InvalidLoginException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNotFoundException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNotFoundException.java new file mode 100644 index 000000000..0f61d8dd5 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/exception/UserNotFoundException.java @@ -0,0 +1,10 @@ +package com.sprint.mission.discodeit.domain.user.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; + +public class UserNotFoundException extends EntityNotFoundException { + public UserNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index e703ecb7b..4bf0d3a3e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -2,6 +2,8 @@ public enum ErrorCode { + // common + NOT_FOUND(404, "요청한 데이터를 찾을 수 없습니다.", "C004"), // User Domain INVALID_USERNAME_LENGTH(400, "유저 이름은 1~32자 이내여야 합니다.", "U001"), @@ -23,7 +25,7 @@ public enum ErrorCode { DUPLICATE_EMAIL(400, "이미 사용중인 이메일입니다.", "U307"), DUPLICATE_USERNAME(400, "이미 존재하는 유저이름입니다.", "U308"), - + INVALID_CREDENTIALS(400, "아이디 또는 비밀번호가 일치하지 않습니다.", "U501") ; private final int status; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java index 34151bac1..d0a18beab 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -6,6 +6,7 @@ import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import org.springframework.stereotype.Repository; @@ -24,6 +25,12 @@ public User save(User user) { return user; } + @Override + public Optional findOneByEmail(Email email) { + User foundUser = emailUsers.get(email); + return Optional.ofNullable(foundUser); + } + @Override public boolean isExistByEmail(Email email) { return emailUsers.containsKey(email); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java index a345c2425..3b32b09d1 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java @@ -3,11 +3,17 @@ import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.domain.user.Username; +import java.util.Optional; +import java.util.UUID; public interface UserRepository { User save(User user); + Optional findOneById(UUID id); + + Optional findOneByEmail(Email email); + boolean isExistByEmail(Email email); boolean isExistByUsername(Username username); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java index 86cfe9bc5..ca925fa81 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java @@ -6,6 +6,7 @@ import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; public class FakeUserRepository implements UserRepository { @@ -22,6 +23,12 @@ public User save(User user) { return user; } + @Override + public Optional findOneByEmail(Email email) { + User foundUser = emailUsers.get(email); + return Optional.ofNullable(foundUser); + } + @Override public boolean isExistByEmail(Email email) { return emailUsers.containsKey(email); diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 63a5a431d..ad605e1a0 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -31,5 +31,7 @@ - `이메일`과 `비밀번호`를 입력받는다. - 유저의 비밀번호가 틀리다면 에러를 반환한다. +- + -- newLine for git From 195cf7880522374b4925ccae8889e307c49b3d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sat, 8 Feb 2025 04:05:41 +0900 Subject: [PATCH 13/38] =?UTF-8?q?fix:=20test=20build=20fail=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 인터페이스 메소드 구현체 구현추가 --- .../discodeit/repository/user/InMemoryUserRepository.java | 8 ++++++-- .../discodeit/fake/repository/FakeUserRepository.java | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java index d0a18beab..07d426bef 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -25,10 +25,14 @@ public User save(User user) { return user; } + @Override + public Optional findOneById(UUID id) { + return Optional.ofNullable(uuidUsers.get(id)); + } + @Override public Optional findOneByEmail(Email email) { - User foundUser = emailUsers.get(email); - return Optional.ofNullable(foundUser); + return Optional.ofNullable(emailUsers.get(email)); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java index ca925fa81..c0e18306f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java @@ -23,10 +23,14 @@ public User save(User user) { return user; } + @Override + public Optional findOneById(UUID id) { + return Optional.ofNullable(uuidUsers.get(id)); + } + @Override public Optional findOneByEmail(Email email) { - User foundUser = emailUsers.get(email); - return Optional.ofNullable(foundUser); + return Optional.ofNullable(emailUsers.get(email)); } @Override From 297bbd7b337da989da8d9758c05d00ef5675fe26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sat, 8 Feb 2025 22:39:30 +0900 Subject: [PATCH 14/38] =?UTF-8?q?refactor:=20method=20name=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 checkAlready -> throwAlready --- .../application/service/user/UserService.java | 8 ++++---- ...5\270 \353\252\250\353\215\270\353\247\201.md" | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java index ee3fc0bf3..524fdc913 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java @@ -39,8 +39,8 @@ public UserService( } public UserResponseDto join(JoinUserReqeustDto requestDto) { - checkEmailAlreadyUsedOrThrow(requestDto.email()); - checkUsernameAlreadyUsedOrThrow(requestDto.username()); + throwEmailAlreadyUsed(requestDto.email()); + throwUsernameAlreadyUsed(requestDto.username()); PasswordValidator.validateOrThrow(requestDto.password()); User savedUser = userRepository.save(toUserWithPasswordEncode(requestDto)); return userConverter.toDto(savedUser); @@ -59,13 +59,13 @@ public User findOneByIdOrThrow(UUID uuid) { .orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND)); } - private void checkEmailAlreadyUsedOrThrow(String email) { + private void throwEmailAlreadyUsed(String email) { if (userRepository.isExistByEmail(new Email(email))) { throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, email); } } - private void checkUsernameAlreadyUsedOrThrow(String username) { + private void throwUsernameAlreadyUsed(String username) { if (userRepository.isExistByUsername(new Username(username))) { throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_USERNAME, username); } diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index ad605e1a0..74d2b3c05 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -31,7 +31,20 @@ - `이메일`과 `비밀번호`를 입력받는다. - 유저의 비밀번호가 틀리다면 에러를 반환한다. -- +- 비밀번호 변경 +- 수신 여부 변경 +## 채널 +- 새로운 채널 생성 + - +- 채널 삭제 +- 채널 정보 수정 + + +## 메세지 +- 새로운 메시지 생성 + - +- 메시지 내용 수정 +- 메시지 삭제 -- newLine for git From 0db550cd6ac6c1fb50aaffddd6aad7a44b990b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sat, 8 Feb 2025 22:51:22 +0900 Subject: [PATCH 15/38] =?UTF-8?q?refactor:=20validate=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=95=88=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discodeit/domain/user/BirthDate.java | 16 +++++- .../mission/discodeit/domain/user/Email.java | 19 ++++++- .../discodeit/domain/user/Nickname.java | 50 ++++++++++++++++++- .../discodeit/domain/user/Username.java | 37 +++++++++++++- .../user/validation/PasswordValidator.java | 2 - 5 files changed, 116 insertions(+), 8 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java index 411cec7b1..56283fb02 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java @@ -1,16 +1,20 @@ package com.sprint.mission.discodeit.domain.user; +import com.sprint.mission.discodeit.domain.user.exception.BirthDateInvalidException; import com.sprint.mission.discodeit.domain.user.validation.BirthDateValidator; +import com.sprint.mission.discodeit.global.error.ErrorCode; import java.time.LocalDate; +import java.util.Objects; import lombok.Getter; @Getter public class BirthDate { + private static final int MIN_AGE_RESTRICT = 13; private final LocalDate value; public BirthDate(LocalDate value) { - BirthDateValidator.validate(value); + validate(value); this.value = value; } @@ -18,4 +22,14 @@ public static BirthDate of(int year, int month, int day) { return new BirthDate(LocalDate.of(year, month, day)); } + public void validate(LocalDate birthDate) { + if (Objects.isNull(birthDate)) { + throw new BirthDateInvalidException(ErrorCode.BIRTHDATE_REQUIRED, ""); + } + LocalDate minimumAllowedYear = LocalDate.now().minusYears(MIN_AGE_RESTRICT); + if (!birthDate.isBefore(minimumAllowedYear)) { + throw new BirthDateInvalidException(ErrorCode.UNDERAGE_SIGNUP_REGISTERED, birthDate.toString()); + } + } + } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java index f8d1cdbe3..9a6e65384 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java @@ -1,6 +1,9 @@ package com.sprint.mission.discodeit.domain.user; -import com.sprint.mission.discodeit.domain.user.validation.EmailValidator; +import com.sprint.mission.discodeit.domain.user.exception.EmailInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; +import java.util.regex.Pattern; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -10,10 +13,22 @@ @ToString(of = {"value"}) public class Email { + private final static String EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z]{2,7})+$"; + private final static Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); + private final String value; public Email(String email) { - EmailValidator.valid(email); + valid(email); this.value = email; } + + public void valid(String email) { + if (Objects.isNull(email) || email.isBlank()) { + throw new EmailInvalidException(ErrorCode.EMAIL_REQUIRED, ""); + } + if (!EMAIL_PATTERN.matcher(email).matches()) { + throw new EmailInvalidException(ErrorCode.INVALID_EMAIL_FORMAT, email); + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java index 9b488f3dd..a03a5cf6a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java @@ -1,6 +1,10 @@ package com.sprint.mission.discodeit.domain.user; -import com.sprint.mission.discodeit.domain.user.validation.NicknameValidator; +import com.sprint.mission.discodeit.domain.user.exception.NickNameInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -10,11 +14,53 @@ @ToString(of = {"value"}) public class Nickname { + private static final int MAX_LENGTH = 32; + private static final Set FORBIDDEN_WORDS = Set.of( + "admin", "moderator", "discord", "system", "root", "bot", "mod", + "운영자", "관리자", "봇" + ); + private static final String VALID_NICK_NAME_REGEX = + "^(?!.*\\s)[\\p{L}\\p{N}\\p{P}\\p{S}\\p{So}]{1,32}$"; + private static final Pattern VALID_NICK_NAME_PATTERN = Pattern.compile(VALID_NICK_NAME_REGEX); + private final String value; public Nickname(String value) { - NicknameValidator.validate(value); + validate(value); this.value = value; } + public void validate(String value) { + throwIsNull(value); + throwOverLength(value); + throwInvalidPattern(value); + throwContainForbiddenWord(value); + } + + private void throwIsNull(String value) { + if (Objects.isNull(value) || value.isBlank()) { + throw new NickNameInvalidException(ErrorCode.NICKNAME_REQUIRED, ""); + } + } + + private void throwOverLength(String value) { + if (value.length() > MAX_LENGTH) { + throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_LENGTH, value); + } + } + + private void throwInvalidPattern(String value) { + if (!VALID_NICK_NAME_PATTERN.matcher(value).matches()) { + throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_FORMAT, value); + } + } + + private void throwContainForbiddenWord(String value) { + String lowerCase = value.toLowerCase(); + for (String word : FORBIDDEN_WORDS) { + if (lowerCase.contains(word)) { + throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_FORMAT, value); + } + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java index 5ca240fc7..d6dab6860 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java @@ -1,6 +1,11 @@ package com.sprint.mission.discodeit.domain.user; +import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; import com.sprint.mission.discodeit.domain.user.validation.UsernameValidator; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -10,11 +15,41 @@ @ToString(of = "value") public class Username { + private static final int MIN_LENGTH = 2; + private static final int MAX_LENGTH = 32; + private static final String VALID_USER_NAME_REGEX = "^(?!.*\\.\\.)(?![.])(?!.*[.]$)[a-zA-Z0-9_.]{2,32}$"; + private static final Pattern VALID_USER_NAME_PATTERN = Pattern.compile(VALID_USER_NAME_REGEX); + private static final Set FORBIDDEN_WORD = + Set.of( + "admin", "moderator", "discord", "system", "root", "bot", "mod", + "운영자", "관리자", "봇" + ); + private final String value; public Username(String value) { - UsernameValidator.validate(value); + validate(value); this.value = value.toLowerCase(); } + public void validate(final String username) { + if (Objects.isNull(username) || username.isBlank()) { + throw new UserNameInvalidException(ErrorCode.USERNAME_REQUIRED, ""); + } + + if (username.length() > MAX_LENGTH || username.length() < MIN_LENGTH) { + throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_LENGTH, username); + } + + if (!VALID_USER_NAME_PATTERN.matcher(username).matches()) { + throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); + } + + String lowerUsername = username.toLowerCase(); + for (String word : FORBIDDEN_WORD) { + if (lowerUsername.contains(word)) { + throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); + } + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java index 9a2bcfe2a..9b45f3949 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java @@ -15,11 +15,9 @@ public static void validateOrThrow(String password) { if (Objects.isNull(password) || password.isEmpty()) { throw new PassWordInvalidException(ErrorCode.PASSWORD_REQUIRED, ""); } - if (password.length() < MIN_PASSWORD_LENGTH || password.length() > MAX_PASSWORD_LENGTH) { throw new PassWordInvalidException(ErrorCode.INVALID_PASSWORD_LENGTH, password); } - if (!PASSWORD_PATTERN.matcher(password).matches()) { throw new PassWordInvalidException(ErrorCode.WEAK_PASSWORD, password); } From 9c38ed31439a5a3279b8a17c34b1b744dcb36916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <9712jw@gmail.com> Date: Sun, 9 Feb 2025 00:37:37 +0900 Subject: [PATCH 16/38] =?UTF-8?q?feat:=20=EC=B1=84=EB=84=90=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B5=AC=ED=98=84,=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5,=20=EC=88=98=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChangeChannelSubjectRequestDto.java | 13 +++++ .../dto/channel/CreateChannelRequestDto.java | 9 ++++ .../dto/user/ChangePasswordRequestDto.java | 9 ++++ .../dto/{ => user}/JoinUserReqeustDto.java | 2 +- .../dto/{ => user}/LoginRequestDto.java | 2 +- .../dto/{ => user}/UserResponseDto.java | 2 +- .../service/channel/ChannelService.java | 38 +++++++++++++ .../application/service/user/UserService.java | 14 +++-- .../service/user/converter/UserConverter.java | 4 +- .../discodeit/domain/channel/Channel.java | 50 ++++++++++++++++++ .../domain/channel/enums/ChannelType.java | 6 +++ .../ChannelNameInvalidException.java | 11 ++++ .../exception/ChannelNotFoundException.java | 11 ++++ .../mission/discodeit/domain/user/User.java | 4 ++ .../discodeit/global/error/ErrorCode.java | 46 ++++++++-------- .../channel/InMemoryChannelRepository.java | 24 +++++++++ .../channel/interfaces/ChannelRepository.java | 12 +++++ .../application/service/UserServiceTest.java | 2 +- .../discodeit/fake/domain/user/StubUser.java | 2 +- .../1-sprint-mission/study/channelDomain.png | Bin 0 -> 36018 bytes ...0 \353\252\250\353\215\270\353\247\201.md" | 15 +++++- 21 files changed, 240 insertions(+), 36 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/ChangePasswordRequestDto.java rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/{ => user}/JoinUserReqeustDto.java (85%) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/{ => user}/LoginRequestDto.java (72%) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/{ => user}/UserResponseDto.java (62%) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelType.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNameInvalidException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNotFoundException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/study/channelDomain.png diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java new file mode 100644 index 000000000..d860ecba4 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.UUID; + +public record ChangeChannelSubjectRequestDto( + @NotNull + UUID channelId, + @Size(max = 1024, message = "채널 주제 길이는 1024이하 제한입니다.") + String subject +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java new file mode 100644 index 000000000..2315ce67c --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import jakarta.validation.constraints.NotBlank; + +public record CreateChannelRequestDto( + @NotBlank String name, + @NotBlank String channelType +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/ChangePasswordRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/ChangePasswordRequestDto.java new file mode 100644 index 000000000..316855981 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/ChangePasswordRequestDto.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.application.dto.user; + +import jakarta.validation.constraints.NotBlank; + +public record ChangePasswordRequestDto( + @NotBlank + String password +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/JoinUserReqeustDto.java similarity index 85% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/JoinUserReqeustDto.java index b6cbf36c8..a8ef94347 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/JoinUserReqeustDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/JoinUserReqeustDto.java @@ -1,4 +1,4 @@ -package com.sprint.mission.discodeit.application.dto; +package com.sprint.mission.discodeit.application.dto.user; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/LoginRequestDto.java similarity index 72% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/LoginRequestDto.java index 64591d96d..c111579e5 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/LoginRequestDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/LoginRequestDto.java @@ -1,4 +1,4 @@ -package com.sprint.mission.discodeit.application.dto; +package com.sprint.mission.discodeit.application.dto.user; import jakarta.validation.constraints.NotBlank; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java similarity index 62% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java index dbe8eb232..cee99c944 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/UserResponseDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java @@ -1,4 +1,4 @@ -package com.sprint.mission.discodeit.application.dto; +package com.sprint.mission.discodeit.application.dto.user; public record UserResponseDto( String nickname, diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java new file mode 100644 index 000000000..42e2d4893 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java @@ -0,0 +1,38 @@ +package com.sprint.mission.discodeit.application.service.channel; + +import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelSubjectRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; +import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import java.util.UUID; + +public class ChannelService { + + private final ChannelRepository channelRepository; + + public ChannelService( + ChannelRepository channelRepository + ) { + this.channelRepository = channelRepository; + } + + public void create(CreateChannelRequestDto requestDto) { + ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); + Channel createChannel = new Channel(requestDto.name(), channelType); + channelRepository.save(createChannel); + } + + public Channel findOneByIdOrThrow(UUID id) { + return channelRepository.findOneById(id) + .orElseThrow(() -> new ChannelNotFoundException(ErrorCode.NOT_FOUND)); + } + + public void changeSubject(ChangeChannelSubjectRequestDto requestDto) { + Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + foundChannel.updateSubject(requestDto.subject()); + channelRepository.save(foundChannel); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java index 524fdc913..e4ccff57e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java @@ -1,9 +1,10 @@ package com.sprint.mission.discodeit.application.service.user; import com.sprint.mission.discodeit.application.auth.PasswordEncoder; -import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; -import com.sprint.mission.discodeit.application.dto.LoginRequestDto; -import com.sprint.mission.discodeit.application.dto.UserResponseDto; +import com.sprint.mission.discodeit.application.dto.user.ChangePasswordRequestDto; +import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.user.LoginRequestDto; +import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; @@ -59,6 +60,13 @@ public User findOneByIdOrThrow(UUID uuid) { .orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND)); } + public void changePassword(UUID userId, ChangePasswordRequestDto requestDto) { + PasswordValidator.validateOrThrow(requestDto.password()); + User foundUser = findOneByIdOrThrow(userId); + foundUser.updatePassword(passwordEncoder.encode(requestDto.password())); + userRepository.save(foundUser); + } + private void throwEmailAlreadyUsed(String email) { if (userRepository.isExistByEmail(new Email(email))) { throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, email); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java index cd6514c8c..c204e5fa0 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java @@ -1,7 +1,7 @@ package com.sprint.mission.discodeit.application.service.user.converter; -import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; -import com.sprint.mission.discodeit.application.dto.UserResponseDto; +import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.Nickname; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java new file mode 100644 index 000000000..541450de9 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -0,0 +1,50 @@ +package com.sprint.mission.discodeit.domain.channel; + +import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; +import com.sprint.mission.discodeit.domain.channel.exception.ChannelNameInvalidException; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public class Channel { + + private final static int SUBJECT_MAX_LENGTH = 1024; + + private final UUID id; + private String name; + private String subject; + private ChannelType type; + private final LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Channel( + String name, + ChannelType type + ) { + validate(name); + this.id = UUID.randomUUID(); + this.name = name; + this.subject = ""; + this.type = type; + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + public void updateSubject(String subject) { + if (subject.length() > SUBJECT_MAX_LENGTH) { + throw new IllegalArgumentException(); + } + this.subject = subject; + } + + private void validate(String name) { + if (Objects.isNull(name) || name.isBlank()) { + throw new ChannelNameInvalidException(ErrorCode.INVALID_CHANNEL_NAME_LENGTH, name); + } + } + + public UUID getId() { + return id; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelType.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelType.java new file mode 100644 index 000000000..010dd58fc --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelType.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.domain.channel.enums; + +public enum ChannelType { + TEXT, + VOICE +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNameInvalidException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNameInvalidException.java new file mode 100644 index 000000000..8e84c9bd1 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNameInvalidException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.channel.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class ChannelNameInvalidException extends InvalidException { + + public ChannelNameInvalidException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNotFoundException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNotFoundException.java new file mode 100644 index 000000000..a5f7e5349 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelNotFoundException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.channel.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; + +public class ChannelNotFoundException extends EntityNotFoundException { + + public ChannelNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java index edf96a5bd..806b11930 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -38,6 +38,10 @@ public User( this.emailSubscriptionStatus = emailSubscriptionStatus; } + public void updatePassword(String encodedPassword) { + password.changePassword(encodedPassword); + } + public UUID getId() { return id; } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index 4bf0d3a3e..c68597c64 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -3,39 +3,40 @@ public enum ErrorCode { // common - NOT_FOUND(404, "요청한 데이터를 찾을 수 없습니다.", "C004"), + NOT_FOUND(404, "요청한 데이터를 찾을 수 없습니다."), - // User Domain - INVALID_USERNAME_LENGTH(400, "유저 이름은 1~32자 이내여야 합니다.", "U001"), - INVALID_NICKNAME_LENGTH(400, "유저 이름은 2~32자 이내여야 합니다.", "U002"), - INVALID_PASSWORD_LENGTH(400, "비밀번호는 8~20자 이내여야 합니다.", "U004"), // 비밀번호 길이 오류 - WEAK_PASSWORD(400, "비밀번호는 대문자, 숫자, 특수문자를 포함해야 합니다.", "U008"), - UNDERAGE_SIGNUP_REGISTERED(400, "13세 이상만 회원가입이 가능합니다.", "U009"), + // User + INVALID_USERNAME_LENGTH(400, "유저 이름은 1~32자 이내여야 합니다."), + INVALID_NICKNAME_LENGTH(400, "유저 이름은 2~32자 이내여야 합니다."), + INVALID_PASSWORD_LENGTH(400, "비밀번호는 8~20자 이내여야 합니다."), + WEAK_PASSWORD(400, "비밀번호는 대문자, 숫자, 특수문자를 포함해야 합니다."), + UNDERAGE_SIGNUP_REGISTERED(400, "13세 이상만 회원가입이 가능합니다."), - USERNAME_REQUIRED(400, "유저 이름은 필수입니다.", "U104"), - NICKNAME_REQUIRED(400, "닉네임은 필수입니다.", "U105"), - EMAIL_REQUIRED(400, "이메일은 필수입니다.", "U106"), - PASSWORD_REQUIRED(400, "비밀번호는 필수 입력값입니다.", "U107"), - BIRTHDATE_REQUIRED(400, "생년월일은 필수 입력값입니다.", "U108"), + USERNAME_REQUIRED(400, "유저 이름은 필수입니다."), + NICKNAME_REQUIRED(400, "닉네임은 필수입니다."), + EMAIL_REQUIRED(400, "이메일은 필수입니다."), + PASSWORD_REQUIRED(400, "비밀번호는 필수 입력값입니다."), + BIRTHDATE_REQUIRED(400, "생년월일은 필수 입력값입니다."), - INVALID_EMAIL_FORMAT(400, "이메일 형식이 올바르지 않습니다", "U203"), - INVALID_USERNAME_FORMAT(400, "유저 이름에 허용되지 않은 문자가 포함되어 있습니다.", "U205"), - INVALID_NICKNAME_FORMAT(400, "닉네임에 허용되지 않은 문자가 포함되어 있습니다.", "U206"), + INVALID_EMAIL_FORMAT(400, "이메일 형식이 올바르지 않습니다"), + INVALID_USERNAME_FORMAT(400, "유저 이름에 허용되지 않은 문자가 포함되어 있습니다."), + INVALID_NICKNAME_FORMAT(400, "닉네임에 허용되지 않은 문자가 포함되어 있습니다."), - DUPLICATE_EMAIL(400, "이미 사용중인 이메일입니다.", "U307"), - DUPLICATE_USERNAME(400, "이미 존재하는 유저이름입니다.", "U308"), + DUPLICATE_EMAIL(400, "이미 사용중인 이메일입니다."), + DUPLICATE_USERNAME(400, "이미 존재하는 유저이름입니다."), - INVALID_CREDENTIALS(400, "아이디 또는 비밀번호가 일치하지 않습니다.", "U501") + INVALID_CREDENTIALS(400, "아이디 또는 비밀번호가 일치하지 않습니다."), + + // Channel + INVALID_CHANNEL_NAME_LENGTH(400, "채널 이름은 필수입니다."), ; private final int status; private final String description; - private final String code; - ErrorCode(int status, String description, String code) { + ErrorCode(int status, String description) { this.status = status; this.description = description; - this.code = code; } public int getStatus() { @@ -46,7 +47,4 @@ public String getDescription() { return description; } - public String getCode() { - return code; - } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java new file mode 100644 index 000000000..911bcc668 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java @@ -0,0 +1,24 @@ +package com.sprint.mission.discodeit.repository.channel; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class InMemoryChannelRepository implements ChannelRepository { + + private final Map uuidChannels = new HashMap<>(); + + @Override + public Channel save(Channel channel) { + Channel savedChannel = uuidChannels.put(channel.getId(), channel); + return channel; + } + + @Override + public Optional findOneById(UUID uuid) { + return Optional.ofNullable(uuidChannels.get(uuid)); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java new file mode 100644 index 000000000..663dffde0 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.repository.channel.interfaces; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import java.util.Optional; +import java.util.UUID; + +public interface ChannelRepository { + + Channel save(Channel channel); + + Optional findOneById(UUID uuid); +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java index 9ad8465df..94f698f1d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.application.service; -import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; import com.sprint.mission.discodeit.application.service.user.UserService; import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; import com.sprint.mission.discodeit.fake.FakeFactory; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java index 3926dd5c2..1daa6d6be 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.fake.domain.user; -import com.sprint.mission.discodeit.application.dto.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.Nickname; diff --git a/codeit-bootcamp-spring/1-sprint-mission/study/channelDomain.png b/codeit-bootcamp-spring/1-sprint-mission/study/channelDomain.png new file mode 100644 index 0000000000000000000000000000000000000000..c3787c682fcbc93666fab8f7423bbe0b22a4042a GIT binary patch literal 36018 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 a&uRD24h`H0?F3{iiK>#OVwt>o*#7~P@I&MP literal 0 HcmV?d00001 diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 74d2b3c05..154699977 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -32,14 +32,25 @@ - 유저의 비밀번호가 틀리다면 에러를 반환한다. - 비밀번호 변경 + - flow + - 본인이 맞는지 기존 비밀번호를 확인한다. + - 변경 비밀번호를 입력받는다. + - 디비에서 유저조회 + - 조횐된 유저의 비밀번호를 암호화 후 변경 + - 디비에 저장 + - 리팩터링할 부분 : 토큰으로 일회용 토큰으로 기존 비밀번호 확인 후 토큰이 없다면 불가하게 하는방법 구현하기 - 수신 여부 변경 ## 채널 - 새로운 채널 생성 - - + - `채널이름`, `채널 유형`을 입력받는다. + - 텍스트 채널 유형 + - 음성, 영상, 화면 유형 + - 채널 주제 (생성 시에는 입력받지 않음, 1024 글자 제한) - 채널 삭제 - 채널 정보 수정 - + - 이름 수정 + - 채널 주제 수정 1024 글자제한 ## 메세지 - 새로운 메시지 생성 From 07a68cd623533a4f9a26c63ebb71b68e44b32263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:55:36 +0900 Subject: [PATCH 17/38] =?UTF-8?q?feat:=20message=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=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 --- .../channel/ChangeChannelNameRequestDto.java | 11 ++++ .../ChangeChannelSubjectRequestDto.java | 4 +- .../dto/channel/DeleteChannelRequestDto.java | 9 ++++ .../dto/message/CreateMessageRequestDto.java | 12 +++++ .../dto/message/MessageResponseDto.java | 19 +++++++ .../service/channel/ChannelService.java | 15 +++++- .../service/message/MessageService.java | 34 ++++++++++++ .../discodeit/domain/channel/Channel.java | 9 +++- .../ChannelSubjectOverLengthException.java | 11 ++++ .../discodeit/domain/message/Message.java | 52 +++++++++++++++++++ .../InvalidMessageContentException.java | 11 ++++ .../discodeit/global/error/ErrorCode.java | 7 ++- .../channel/InMemoryChannelRepository.java | 5 ++ .../channel/interfaces/ChannelRepository.java | 2 + .../message/interfaces/MessageRepository.java | 9 ++++ ...0 \353\252\250\353\215\270\353\247\201.md" | 4 +- 16 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelNameRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/DeleteChannelRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/CreateMessageRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/MessageResponseDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelSubjectOverLengthException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/InvalidMessageContentException.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelNameRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelNameRequestDto.java new file mode 100644 index 000000000..757171558 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelNameRequestDto.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +public record ChangeChannelNameRequestDto( + @NotNull UUID channelId, + @NotBlank String channelName +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java index d860ecba4..d5dea9a01 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChangeChannelSubjectRequestDto.java @@ -5,8 +5,8 @@ import java.util.UUID; public record ChangeChannelSubjectRequestDto( - @NotNull - UUID channelId, + @NotNull UUID channelId, + @Size(max = 1024, message = "채널 주제 길이는 1024이하 제한입니다.") String subject ) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/DeleteChannelRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/DeleteChannelRequestDto.java new file mode 100644 index 000000000..4a89ca69c --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/DeleteChannelRequestDto.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +public record DeleteChannelRequestDto( + @NotNull UUID channelId +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/CreateMessageRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/CreateMessageRequestDto.java new file mode 100644 index 000000000..2b13aa643 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/CreateMessageRequestDto.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.application.dto.message; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +public record CreateMessageRequestDto( + @NotBlank String content, + @NotNull UUID userId, + @NotNull UUID destinationChannelId +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/MessageResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/MessageResponseDto.java new file mode 100644 index 000000000..10af2d9cd --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/MessageResponseDto.java @@ -0,0 +1,19 @@ +package com.sprint.mission.discodeit.application.dto.message; + +import com.sprint.mission.discodeit.domain.message.Message; +import java.util.UUID; + +public record MessageResponseDto( + UUID messageId, + String content, + String senderName, + UUID destinationChannelId +) { + public static MessageResponseDto from(Message message) { + return new MessageResponseDto( + message.getId(), + message.getSenderName(), + message.getContent(), + message.getDestinationChannelId()); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java index 42e2d4893..531261e1e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java @@ -1,7 +1,9 @@ package com.sprint.mission.discodeit.application.service.channel; +import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelNameRequestDto; import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelSubjectRequestDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; @@ -20,7 +22,7 @@ public ChannelService( } public void create(CreateChannelRequestDto requestDto) { - ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); + ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); // TODO 예외 발생할 수 있음 -> 이넘타입에 존재하지 않을 경우 Channel createChannel = new Channel(requestDto.name(), channelType); channelRepository.save(createChannel); } @@ -35,4 +37,15 @@ public void changeSubject(ChangeChannelSubjectRequestDto requestDto) { foundChannel.updateSubject(requestDto.subject()); channelRepository.save(foundChannel); } + + public void changeChannelName(ChangeChannelNameRequestDto requestDto) { + Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + foundChannel.updateName(requestDto.channelName()); + channelRepository.save(foundChannel); + } + + public void deleteChannel(DeleteChannelRequestDto requestDto) { + Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + channelRepository.deleteById(foundChannel.getId()); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java new file mode 100644 index 000000000..58f3bd154 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java @@ -0,0 +1,34 @@ +package com.sprint.mission.discodeit.application.service.message; + +import com.sprint.mission.discodeit.application.dto.message.CreateMessageRequestDto; +import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; +import com.sprint.mission.discodeit.application.service.channel.ChannelService; +import com.sprint.mission.discodeit.application.service.user.UserService; +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.message.Message; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; + +public class MessageService { + + private final MessageRepository messageRepository; + private final ChannelService channelService; + private final UserService userService; + + public MessageService( + MessageRepository messageRepository, + ChannelService channelService, + UserService userService + ) { + this.messageRepository = messageRepository; + this.channelService = channelService; + this.userService = userService; + } + + public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { + User sender = userService.findOneByIdOrThrow(requestDto.userId()); + Channel destinationChannel = channelService.findOneByIdOrThrow(requestDto.destinationChannelId()); + Message createMessage = messageRepository.save(new Message(sender, destinationChannel, requestDto.content())); + return MessageResponseDto.from(createMessage); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index 541450de9..a209e9e28 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNameInvalidException; +import com.sprint.mission.discodeit.domain.channel.exception.ChannelSubjectOverLengthException; import com.sprint.mission.discodeit.global.error.ErrorCode; import java.time.LocalDateTime; import java.util.Objects; @@ -33,14 +34,18 @@ public Channel( public void updateSubject(String subject) { if (subject.length() > SUBJECT_MAX_LENGTH) { - throw new IllegalArgumentException(); + throw new ChannelSubjectOverLengthException(ErrorCode.INVALID_SUBJECT_LENGTH, "입력 글자 수:".concat(String.valueOf(subject.length()))); } this.subject = subject; } + public void updateName(String name) { + this.name = name.trim(); + } + private void validate(String name) { if (Objects.isNull(name) || name.isBlank()) { - throw new ChannelNameInvalidException(ErrorCode.INVALID_CHANNEL_NAME_LENGTH, name); + throw new ChannelNameInvalidException(ErrorCode.INVALID_CHANNEL_NAME_NOT_NULL, name); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelSubjectOverLengthException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelSubjectOverLengthException.java new file mode 100644 index 000000000..054e6fa7d --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/ChannelSubjectOverLengthException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.channel.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class ChannelSubjectOverLengthException extends InvalidException { + + public ChannelSubjectOverLengthException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java new file mode 100644 index 000000000..cabcb1d80 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java @@ -0,0 +1,52 @@ +package com.sprint.mission.discodeit.domain.message; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.message.exception.InvalidMessageContentException; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public class Message { + + private final UUID id; + private final LocalDateTime createdAt; + private LocalDateTime updatedAt; + private final User sender; + private final Channel destinationChannel; + private String content; + + public Message(User sender, Channel destinationChannel, String content) { + validate(content); + this.id = UUID.randomUUID(); + this.sender = sender; + this.destinationChannel = destinationChannel; + this.content = content; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + private void validate(String content) { + if (Objects.isNull(content) || content.isBlank()) { + throw new InvalidMessageContentException(ErrorCode.INVALID_MESSAGE_CONTENT_NOT_NULL, content); + } + } + + public UUID getId() { + return id; + } + + public String getContent() { + return content; + } + + public String getSenderName() { + return sender.getNicknameValue(); + } + + public UUID getDestinationChannelId() { + return destinationChannel.getId(); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/InvalidMessageContentException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/InvalidMessageContentException.java new file mode 100644 index 000000000..5739ff452 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/InvalidMessageContentException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.message.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class InvalidMessageContentException extends InvalidException { + + public InvalidMessageContentException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index c68597c64..94e5dedaa 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -4,7 +4,7 @@ public enum ErrorCode { // common NOT_FOUND(404, "요청한 데이터를 찾을 수 없습니다."), - + INVALID_SUBJECT_LENGTH(400, "채널 주제는 최대 1024자 이내여야 합니다."), // User INVALID_USERNAME_LENGTH(400, "유저 이름은 1~32자 이내여야 합니다."), INVALID_NICKNAME_LENGTH(400, "유저 이름은 2~32자 이내여야 합니다."), @@ -28,7 +28,10 @@ public enum ErrorCode { INVALID_CREDENTIALS(400, "아이디 또는 비밀번호가 일치하지 않습니다."), // Channel - INVALID_CHANNEL_NAME_LENGTH(400, "채널 이름은 필수입니다."), + INVALID_CHANNEL_NAME_NOT_NULL(400, "채널 이름은 필수 입력값 입니다."), + + // Message + INVALID_MESSAGE_CONTENT_NOT_NULL(400, "메시지 내용은 필수 입력값 입니다."), ; private final int status; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java index 911bcc668..b133fb177 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java @@ -21,4 +21,9 @@ public Channel save(Channel channel) { public Optional findOneById(UUID uuid) { return Optional.ofNullable(uuidChannels.get(uuid)); } + + @Override + public void deleteById(UUID uuid) { + uuidChannels.remove(uuid); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java index 663dffde0..767a7f1ee 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java @@ -9,4 +9,6 @@ public interface ChannelRepository { Channel save(Channel channel); Optional findOneById(UUID uuid); + + void deleteById(UUID uuid); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java new file mode 100644 index 000000000..078dd530b --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.repository.message.interfaces; + +import com.sprint.mission.discodeit.domain.message.Message; + +public interface MessageRepository { + + Message save(Message message); + +} diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 154699977..96928068b 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -53,8 +53,10 @@ - 채널 주제 수정 1024 글자제한 ## 메세지 + - 새로운 메시지 생성 - - + - `메시지 id`, `메시지 생성, 수정시간`, `메시지 내용`, `보낸사람`, `목적지 채널` + - 메시지 내용은 비어있을 수 없다. - 메시지 내용 수정 - 메시지 삭제 From 94f27cb1cae991bfe67a80d6b52aa88284ac479a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:03:17 +0900 Subject: [PATCH 18/38] =?UTF-8?q?feat:=20message=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=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 --- .../dto/message/DeleteMessageRequestDto.java | 9 +++++++++ .../UpdateMessageContentRequestDto.java | 11 ++++++++++ .../service/message/MessageService.java | 20 +++++++++++++++++++ .../discodeit/domain/message/Message.java | 5 +++++ .../exception/MessageNotFoundException.java | 11 ++++++++++ .../message/interfaces/MessageRepository.java | 5 +++++ 6 files changed, 61 insertions(+) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/DeleteMessageRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/UpdateMessageContentRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/MessageNotFoundException.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/DeleteMessageRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/DeleteMessageRequestDto.java new file mode 100644 index 000000000..31656c421 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/DeleteMessageRequestDto.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.application.dto.message; + +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +public record DeleteMessageRequestDto( + @NotNull UUID messageId +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/UpdateMessageContentRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/UpdateMessageContentRequestDto.java new file mode 100644 index 000000000..615039b16 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/message/UpdateMessageContentRequestDto.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.application.dto.message; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +public record UpdateMessageContentRequestDto( + @NotNull UUID messageId, + @NotBlank String content +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java index 58f3bd154..0b6d660d9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java @@ -1,13 +1,18 @@ package com.sprint.mission.discodeit.application.service.message; import com.sprint.mission.discodeit.application.dto.message.CreateMessageRequestDto; +import com.sprint.mission.discodeit.application.dto.message.DeleteMessageRequestDto; import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; +import com.sprint.mission.discodeit.application.dto.message.UpdateMessageContentRequestDto; import com.sprint.mission.discodeit.application.service.channel.ChannelService; import com.sprint.mission.discodeit.application.service.user.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; +import com.sprint.mission.discodeit.domain.message.exception.MessageNotFoundException; import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.util.UUID; public class MessageService { @@ -31,4 +36,19 @@ public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { Message createMessage = messageRepository.save(new Message(sender, destinationChannel, requestDto.content())); return MessageResponseDto.from(createMessage); } + + public void updateMessage(UpdateMessageContentRequestDto requestDto) { + Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); + foundMessage.updateContent(requestDto.content()); + messageRepository.save(foundMessage); + } + + public void deleteMessage(DeleteMessageRequestDto requestDto) { + Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); + messageRepository.deleteById(foundMessage.getId()); + } + + public Message findOneByIdOrThrow(UUID messageId) { + return messageRepository.findById(messageId).orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java index cabcb1d80..d1a9550a4 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java @@ -27,6 +27,11 @@ public Message(User sender, Channel destinationChannel, String content) { this.updatedAt = LocalDateTime.now(); } + public void updateContent(String content) { + validate(content); + this.content = content; + } + private void validate(String content) { if (Objects.isNull(content) || content.isBlank()) { throw new InvalidMessageContentException(ErrorCode.INVALID_MESSAGE_CONTENT_NOT_NULL, content); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/MessageNotFoundException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/MessageNotFoundException.java new file mode 100644 index 000000000..718cdbad1 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/exception/MessageNotFoundException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.message.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; + +public class MessageNotFoundException extends EntityNotFoundException { + + public MessageNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java index 078dd530b..252d39325 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java @@ -1,9 +1,14 @@ package com.sprint.mission.discodeit.repository.message.interfaces; import com.sprint.mission.discodeit.domain.message.Message; +import java.util.Optional; +import java.util.UUID; public interface MessageRepository { Message save(Message message); + Optional findById(UUID uuid); + + void deleteById(UUID uuid); } From 25330760cd484f4a0ca82263d0f4c0ae9e3173ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:21:18 +0900 Subject: [PATCH 19/38] =?UTF-8?q?refactor:=20service=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/channel/ChannelResponseDto.java | 21 ++++++++++++++++++ ...eqeustDto.java => joinUserRequestDto.java} | 2 +- ...nelService.java => JCFChannelService.java} | 16 ++++++++++---- .../service/interfaces/ChannelService.java | 22 +++++++++++++++++++ .../service/interfaces/MessageService.java | 19 ++++++++++++++++ .../service/interfaces/UserService.java | 20 +++++++++++++++++ ...ageService.java => JCFMessageService.java} | 21 +++++++++++------- .../{UserService.java => JCFUserService.java} | 15 ++++++++----- .../service/user/converter/UserConverter.java | 4 ++-- .../discodeit/domain/channel/Channel.java | 15 ++++++++++++- .../application/service/UserServiceTest.java | 12 +++++----- .../mission/discodeit/fake/FakeFactory.java | 6 ++--- .../discodeit/fake/domain/user/StubUser.java | 14 ++++++------ 13 files changed, 150 insertions(+), 37 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/{JoinUserReqeustDto.java => joinUserRequestDto.java} (91%) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/{ChannelService.java => JCFChannelService.java} (80%) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/{MessageService.java => JCFMessageService.java} (81%) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/{UserService.java => JCFUserService.java} (91%) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java new file mode 100644 index 000000000..2aafb3304 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java @@ -0,0 +1,21 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import java.util.UUID; + +public record ChannelResponseDto( + UUID channelId, + String name, + String subject, + String channelType +) { + + public static ChannelResponseDto from(Channel channel) { + return new ChannelResponseDto( + channel.getId(), + channel.getName(), + channel.getSubject(), + channel.getType() + ); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/JoinUserReqeustDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/joinUserRequestDto.java similarity index 91% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/JoinUserReqeustDto.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/joinUserRequestDto.java index a8ef94347..d5f7c11d2 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/JoinUserReqeustDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/joinUserRequestDto.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -public record JoinUserReqeustDto( +public record joinUserRequestDto( @NotBlank String nickname, @NotBlank String username, @Email String email, diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java similarity index 80% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index 531261e1e..b1a7d1e02 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/ChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -2,8 +2,10 @@ import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelNameRequestDto; import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelSubjectRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.ChannelResponseDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; +import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; @@ -11,39 +13,45 @@ import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import java.util.UUID; -public class ChannelService { +public class JCFChannelService implements ChannelService { private final ChannelRepository channelRepository; - public ChannelService( + public JCFChannelService( ChannelRepository channelRepository ) { this.channelRepository = channelRepository; } - public void create(CreateChannelRequestDto requestDto) { + @Override + public ChannelResponseDto create(CreateChannelRequestDto requestDto) { ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); // TODO 예외 발생할 수 있음 -> 이넘타입에 존재하지 않을 경우 Channel createChannel = new Channel(requestDto.name(), channelType); - channelRepository.save(createChannel); + Channel savedChannel = channelRepository.save(createChannel); + return ChannelResponseDto.from(savedChannel); } + @Override public Channel findOneByIdOrThrow(UUID id) { return channelRepository.findOneById(id) .orElseThrow(() -> new ChannelNotFoundException(ErrorCode.NOT_FOUND)); } + @Override public void changeSubject(ChangeChannelSubjectRequestDto requestDto) { Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); foundChannel.updateSubject(requestDto.subject()); channelRepository.save(foundChannel); } + @Override public void changeChannelName(ChangeChannelNameRequestDto requestDto) { Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); foundChannel.updateName(requestDto.channelName()); channelRepository.save(foundChannel); } + @Override public void deleteChannel(DeleteChannelRequestDto requestDto) { Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); channelRepository.deleteById(foundChannel.getId()); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java new file mode 100644 index 000000000..2f09e5671 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java @@ -0,0 +1,22 @@ +package com.sprint.mission.discodeit.application.service.interfaces; + +import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelNameRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelSubjectRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.ChannelResponseDto; +import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; +import com.sprint.mission.discodeit.domain.channel.Channel; +import java.util.UUID; + +public interface ChannelService { + + ChannelResponseDto create(CreateChannelRequestDto requestDto); + + Channel findOneByIdOrThrow(UUID uuid); + + void changeSubject(ChangeChannelSubjectRequestDto requestDto); + + void changeChannelName(ChangeChannelNameRequestDto requestDto); + + void deleteChannel(DeleteChannelRequestDto requestDto); +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java new file mode 100644 index 000000000..f68063458 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java @@ -0,0 +1,19 @@ +package com.sprint.mission.discodeit.application.service.interfaces; + +import com.sprint.mission.discodeit.application.dto.message.CreateMessageRequestDto; +import com.sprint.mission.discodeit.application.dto.message.DeleteMessageRequestDto; +import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; +import com.sprint.mission.discodeit.application.dto.message.UpdateMessageContentRequestDto; +import com.sprint.mission.discodeit.domain.message.Message; +import java.util.UUID; + +public interface MessageService { + + MessageResponseDto createMessage(CreateMessageRequestDto requestDto); + + void updateMessage(UpdateMessageContentRequestDto requestDto); + + void deleteMessage(DeleteMessageRequestDto requestDto); + + Message findOneByIdOrThrow(UUID uuid); +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java new file mode 100644 index 000000000..ca3654f36 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.application.service.interfaces; + +import com.sprint.mission.discodeit.application.dto.user.ChangePasswordRequestDto; +import com.sprint.mission.discodeit.application.dto.user.LoginRequestDto; +import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; +import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; +import com.sprint.mission.discodeit.domain.user.User; +import java.util.UUID; + +public interface UserService { + + UserResponseDto join(joinUserRequestDto requestDto); + + void login(LoginRequestDto requestDto); + + User findOneByIdOrThrow(UUID userId); + + void changePassword(UUID userId, ChangePasswordRequestDto requestDto); + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java similarity index 81% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index 0b6d660d9..f1f2cbe43 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/MessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -4,8 +4,9 @@ import com.sprint.mission.discodeit.application.dto.message.DeleteMessageRequestDto; import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; import com.sprint.mission.discodeit.application.dto.message.UpdateMessageContentRequestDto; -import com.sprint.mission.discodeit.application.service.channel.ChannelService; -import com.sprint.mission.discodeit.application.service.user.UserService; +import com.sprint.mission.discodeit.application.service.channel.JCFChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.MessageService; +import com.sprint.mission.discodeit.application.service.user.JCFUserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; import com.sprint.mission.discodeit.domain.message.exception.MessageNotFoundException; @@ -14,22 +15,23 @@ import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; import java.util.UUID; -public class MessageService { +public class JCFMessageService implements MessageService { private final MessageRepository messageRepository; - private final ChannelService channelService; - private final UserService userService; + private final JCFChannelService channelService; + private final JCFUserService userService; - public MessageService( + public JCFMessageService( MessageRepository messageRepository, - ChannelService channelService, - UserService userService + JCFChannelService channelService, + JCFUserService userService ) { this.messageRepository = messageRepository; this.channelService = channelService; this.userService = userService; } + @Override public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { User sender = userService.findOneByIdOrThrow(requestDto.userId()); Channel destinationChannel = channelService.findOneByIdOrThrow(requestDto.destinationChannelId()); @@ -37,17 +39,20 @@ public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { return MessageResponseDto.from(createMessage); } + @Override public void updateMessage(UpdateMessageContentRequestDto requestDto) { Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); foundMessage.updateContent(requestDto.content()); messageRepository.save(foundMessage); } + @Override public void deleteMessage(DeleteMessageRequestDto requestDto) { Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); messageRepository.deleteById(foundMessage.getId()); } + @Override public Message findOneByIdOrThrow(UUID messageId) { return messageRepository.findById(messageId).orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java similarity index 91% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java index e4ccff57e..73ddc6646 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java @@ -2,9 +2,10 @@ import com.sprint.mission.discodeit.application.auth.PasswordEncoder; import com.sprint.mission.discodeit.application.dto.user.ChangePasswordRequestDto; -import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; import com.sprint.mission.discodeit.application.dto.user.LoginRequestDto; import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; +import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; +import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; @@ -23,13 +24,13 @@ import org.springframework.stereotype.Service; @Service -public class UserService { +public class JCFUserService implements UserService { private final UserRepository userRepository; private final UserConverter userConverter; private final PasswordEncoder passwordEncoder; - public UserService( + public JCFUserService( UserRepository userRepository, UserConverter userConverter, PasswordEncoder passwordEncoder @@ -39,7 +40,8 @@ public UserService( this.passwordEncoder = passwordEncoder; } - public UserResponseDto join(JoinUserReqeustDto requestDto) { + @Override + public UserResponseDto join(joinUserRequestDto requestDto) { throwEmailAlreadyUsed(requestDto.email()); throwUsernameAlreadyUsed(requestDto.username()); PasswordValidator.validateOrThrow(requestDto.password()); @@ -47,6 +49,7 @@ public UserResponseDto join(JoinUserReqeustDto requestDto) { return userConverter.toDto(savedUser); } + @Override public void login(LoginRequestDto requestDto) { userRepository.findOneByEmail(new Email(requestDto.email())) .filter(user -> matchUserPassword(requestDto.password(), user.getPasswordValue())) @@ -55,11 +58,13 @@ public void login(LoginRequestDto requestDto) { // etc. 스프링 시큐리티 적용 시 리팩터링 최대한 변경에 유연하게 코딩 } + @Override public User findOneByIdOrThrow(UUID uuid) { return userRepository.findOneById(uuid) .orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND)); } + @Override public void changePassword(UUID userId, ChangePasswordRequestDto requestDto) { PasswordValidator.validateOrThrow(requestDto.password()); User foundUser = findOneByIdOrThrow(userId); @@ -83,7 +88,7 @@ private boolean matchUserPassword(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword, encodedPassword); } - private User toUserWithPasswordEncode(JoinUserReqeustDto requestDto) { + private User toUserWithPasswordEncode(joinUserRequestDto requestDto) { return User.builder() .username(new Username(requestDto.username())) .nickname(new Nickname(requestDto.nickname())) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java index c204e5fa0..fc0c7f045 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.application.service.user.converter; -import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; @@ -14,7 +14,7 @@ @Component public class UserConverter { - public User toUser(JoinUserReqeustDto requestDto) { + public User toUser(joinUserRequestDto requestDto) { return User.builder() .username(new Username(requestDto.username())) .nickname(new Nickname(requestDto.nickname())) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index a209e9e28..7fbc842eb 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -34,7 +34,8 @@ public Channel( public void updateSubject(String subject) { if (subject.length() > SUBJECT_MAX_LENGTH) { - throw new ChannelSubjectOverLengthException(ErrorCode.INVALID_SUBJECT_LENGTH, "입력 글자 수:".concat(String.valueOf(subject.length()))); + throw new ChannelSubjectOverLengthException(ErrorCode.INVALID_SUBJECT_LENGTH, + "입력 글자 수:".concat(String.valueOf(subject.length()))); } this.subject = subject; } @@ -52,4 +53,16 @@ private void validate(String name) { public UUID getId() { return id; } + + public String getName() { + return name; + } + + public String getSubject() { + return subject; + } + + public String getType() { + return type.toString(); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java index 94f698f1d..3506b642b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/application/service/UserServiceTest.java @@ -1,7 +1,7 @@ package com.sprint.mission.discodeit.application.service; -import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; -import com.sprint.mission.discodeit.application.service.user.UserService; +import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; +import com.sprint.mission.discodeit.application.service.user.JCFUserService; import com.sprint.mission.discodeit.domain.user.exception.AlreadyUserExistsException; import com.sprint.mission.discodeit.fake.FakeFactory; import com.sprint.mission.discodeit.fake.domain.user.StubUser; @@ -11,8 +11,8 @@ class UserServiceTest { - private UserService userService; - private JoinUserReqeustDto joinUserReqeustDto = StubUser.generateJoinRequestDto(); + private JCFUserService userService; + private joinUserRequestDto joinUserReqeustDto = StubUser.generateJoinRequestDto(); @BeforeEach void setup() { @@ -23,7 +23,7 @@ void setup() { void 회원가입_요청_메소드_호출_이미_존재하는_이메일_이면_에러throw() { // given userService.join(joinUserReqeustDto); - JoinUserReqeustDto anotherUsernameRequest = StubUser.generateJoinRequestByUsername("anotherusername"); + joinUserRequestDto anotherUsernameRequest = StubUser.generateJoinRequestByUsername("anotherusername"); // when Throwable catchThrow = Assertions.catchThrowable( @@ -36,7 +36,7 @@ void setup() { void 회원가입_요청_이미_존재하는_유저이름_요청_에러throw() { // given userService.join(joinUserReqeustDto); - JoinUserReqeustDto anotherEmailRequest = StubUser.generateJoinRequestByEmail("another@test.com"); + joinUserRequestDto anotherEmailRequest = StubUser.generateJoinRequestByEmail("another@test.com"); // when Throwable catchThrow = Assertions.catchThrowable( diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java index 0a3f22b6f..d3da490c5 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java @@ -1,7 +1,7 @@ package com.sprint.mission.discodeit.fake; import com.sprint.mission.discodeit.application.auth.PasswordEncoder; -import com.sprint.mission.discodeit.application.service.user.UserService; +import com.sprint.mission.discodeit.application.service.user.JCFUserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.fake.repository.FakeUserRepository; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; @@ -14,7 +14,7 @@ public static UserRepository getUserRepository() { return new FakeUserRepository(); } - public static UserService getUserService() { - return new UserService(getUserRepository(), new UserConverter(), new PasswordEncoder()); + public static JCFUserService getUserService() { + return new JCFUserService(getUserRepository(), new UserConverter(), new PasswordEncoder()); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java index 1daa6d6be..5e5614782 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.fake.domain.user; -import com.sprint.mission.discodeit.application.dto.user.JoinUserReqeustDto; +import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.Nickname; @@ -29,15 +29,15 @@ public static User generateUser() { .build(); } - public static JoinUserReqeustDto generateJoinRequestDto() { - return new JoinUserReqeustDto(NICK_NAME, USER_NAME, EMAIL, PASSWORD, BIRTH_DATE); + public static joinUserRequestDto generateJoinRequestDto() { + return new joinUserRequestDto(NICK_NAME, USER_NAME, EMAIL, PASSWORD, BIRTH_DATE); } - public static JoinUserReqeustDto generateJoinRequestByUsername(String username) { - return new JoinUserReqeustDto(NICK_NAME, username, EMAIL, PASSWORD, BIRTH_DATE); + public static joinUserRequestDto generateJoinRequestByUsername(String username) { + return new joinUserRequestDto(NICK_NAME, username, EMAIL, PASSWORD, BIRTH_DATE); } - public static JoinUserReqeustDto generateJoinRequestByEmail(String email) { - return new JoinUserReqeustDto(NICK_NAME, USER_NAME, email, PASSWORD, BIRTH_DATE); + public static joinUserRequestDto generateJoinRequestByEmail(String email) { + return new joinUserRequestDto(NICK_NAME, USER_NAME, email, PASSWORD, BIRTH_DATE); } } From 485c7fbc892c30708b51773bdbc48091284461a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:23:11 +0900 Subject: [PATCH 20/38] =?UTF-8?q?feat:=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=EC=9D=84=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 --- .../mission/discodeit/JavaApplication.java | 17 +++++++++ .../service/message/JCFMessageService.java | 15 ++++---- .../discodeit/config/ChannelFactory.java | 33 +++++++++++++++++ .../discodeit/config/MessageFactory.java | 33 +++++++++++++++++ .../mission/discodeit/config/UserFactory.java | 35 +++++++++++++++++++ .../src/main/resources/application.yaml | 1 - 6 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java new file mode 100644 index 000000000..31ed5e30b --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit; + +import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.MessageService; +import com.sprint.mission.discodeit.application.service.interfaces.UserService; +import com.sprint.mission.discodeit.config.ChannelFactory; +import com.sprint.mission.discodeit.config.MessageFactory; +import com.sprint.mission.discodeit.config.UserFactory; + +public class JavaApplication { + + public static void main(String[] args) { + UserService userService = UserFactory.getUserService(); + ChannelService channelService = ChannelFactory.getChannelService(); + MessageService messageService = MessageFactory.getMessageService(); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index f1f2cbe43..c67206298 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -4,9 +4,9 @@ import com.sprint.mission.discodeit.application.dto.message.DeleteMessageRequestDto; import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; import com.sprint.mission.discodeit.application.dto.message.UpdateMessageContentRequestDto; -import com.sprint.mission.discodeit.application.service.channel.JCFChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.MessageService; -import com.sprint.mission.discodeit.application.service.user.JCFUserService; +import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; import com.sprint.mission.discodeit.domain.message.exception.MessageNotFoundException; @@ -18,13 +18,13 @@ public class JCFMessageService implements MessageService { private final MessageRepository messageRepository; - private final JCFChannelService channelService; - private final JCFUserService userService; + private final ChannelService channelService; + private final UserService userService; public JCFMessageService( MessageRepository messageRepository, - JCFChannelService channelService, - JCFUserService userService + ChannelService channelService, + UserService userService ) { this.messageRepository = messageRepository; this.channelService = channelService; @@ -54,6 +54,7 @@ public void deleteMessage(DeleteMessageRequestDto requestDto) { @Override public Message findOneByIdOrThrow(UUID messageId) { - return messageRepository.findById(messageId).orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); + return messageRepository.findById(messageId) + .orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java new file mode 100644 index 000000000..574e3b5ad --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java @@ -0,0 +1,33 @@ +package com.sprint.mission.discodeit.config; + +import com.sprint.mission.discodeit.application.service.channel.JCFChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; +import com.sprint.mission.discodeit.repository.channel.InMemoryChannelRepository; +import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class ChannelFactory { + + private static final ChannelRepository CHANNEL_REPOSITORY = createChannelRepository(); + private static final ChannelService CHANNEL_SERVICE = new JCFChannelService(CHANNEL_REPOSITORY); + + public static ChannelService getChannelService() { + return CHANNEL_SERVICE; + } + + public static ChannelRepository createChannelRepository() { + Properties props = new Properties(); + try { + props.load(new FileInputStream("application.yaml")); + } catch (IOException exception) { + System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); + } + String repositoryType = props.getProperty("repository.type", "memory"); + + return switch (repositoryType) { + default -> new InMemoryChannelRepository(); + }; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java new file mode 100644 index 000000000..0cd710730 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java @@ -0,0 +1,33 @@ +package com.sprint.mission.discodeit.config; + +import com.sprint.mission.discodeit.application.service.interfaces.MessageService; +import com.sprint.mission.discodeit.application.service.message.JCFMessageService; +import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class MessageFactory { + + private static final MessageRepository MESSAGE_REPOSITORY = createMessageRepository(); + private static final MessageService MESSAGE_SERVICE = new JCFMessageService(MESSAGE_REPOSITORY, + ChannelFactory.getChannelService(), UserFactory.getUserService()); + + public static MessageService getMessageService() { + return MESSAGE_SERVICE; + } + + public static MessageRepository createMessageRepository() { + Properties properties = new Properties(); + try { + properties.load(new FileInputStream("application.yml")); + } catch (IOException e) { + System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); + } + String repositoryType = properties.getProperty("repository.type", "memory"); + + return switch (repositoryType) { + default -> MESSAGE_REPOSITORY; + }; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java new file mode 100644 index 000000000..e2f8c45e4 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java @@ -0,0 +1,35 @@ +package com.sprint.mission.discodeit.config; + +import com.sprint.mission.discodeit.application.auth.PasswordEncoder; +import com.sprint.mission.discodeit.application.service.interfaces.UserService; +import com.sprint.mission.discodeit.application.service.user.JCFUserService; +import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; +import com.sprint.mission.discodeit.repository.user.InMemoryUserRepository; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class UserFactory { + + private static final UserRepository USER_REPOSITORY = createUserRepository(); + private static final UserService USER_SERVICE = new JCFUserService(USER_REPOSITORY, new UserConverter(), new PasswordEncoder()); + + public static UserService getUserService() { + return USER_SERVICE; + } + + public static UserRepository createUserRepository() { + Properties props = new Properties(); + try { + props.load(new FileInputStream("application.yaml")); + } catch (IOException exception) { + System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); + } + String repositoryType = props.getProperty("repository.type", "memory"); + + return switch(repositoryType) { + default -> new InMemoryUserRepository(); + }; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml index 545cf9f09..e69de29bb 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml @@ -1 +0,0 @@ -spring.application.name=discodeit From 74be0d4d4a2164b1bd61e4726683bbffbeb6087d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:31:22 +0900 Subject: [PATCH 21/38] refactor: yaml -> properties --- .../sprint/mission/discodeit/config/ChannelFactory.java | 9 +++++---- .../sprint/mission/discodeit/config/MessageFactory.java | 9 +++++---- .../com/sprint/mission/discodeit/config/UserFactory.java | 9 +++++---- .../src/main/resources/application.properties | 1 + .../1-sprint-mission/src/main/resources/application.yaml | 0 5 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java index 574e3b5ad..7efac8859 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java @@ -6,6 +6,7 @@ import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Properties; public class ChannelFactory { @@ -18,13 +19,13 @@ public static ChannelService getChannelService() { } public static ChannelRepository createChannelRepository() { - Properties props = new Properties(); - try { - props.load(new FileInputStream("application.yaml")); + Properties properties = new Properties(); + try (InputStream is = ChannelFactory.class.getClassLoader().getResourceAsStream("application.properties")) { + properties.load(is); } catch (IOException exception) { System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); } - String repositoryType = props.getProperty("repository.type", "memory"); + String repositoryType = properties.getProperty("repository.type", "memory"); return switch (repositoryType) { default -> new InMemoryChannelRepository(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java index 0cd710730..6c895346e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java @@ -5,13 +5,14 @@ import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Properties; public class MessageFactory { private static final MessageRepository MESSAGE_REPOSITORY = createMessageRepository(); - private static final MessageService MESSAGE_SERVICE = new JCFMessageService(MESSAGE_REPOSITORY, - ChannelFactory.getChannelService(), UserFactory.getUserService()); + private static final MessageService MESSAGE_SERVICE = + new JCFMessageService(MESSAGE_REPOSITORY, ChannelFactory.getChannelService(), UserFactory.getUserService()); public static MessageService getMessageService() { return MESSAGE_SERVICE; @@ -19,8 +20,8 @@ public static MessageService getMessageService() { public static MessageRepository createMessageRepository() { Properties properties = new Properties(); - try { - properties.load(new FileInputStream("application.yml")); + try (InputStream is = ChannelFactory.class.getClassLoader().getResourceAsStream("application.properties")) { + properties.load(is); } catch (IOException e) { System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java index e2f8c45e4..88458468e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java @@ -8,6 +8,7 @@ import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Properties; public class UserFactory { @@ -20,13 +21,13 @@ public static UserService getUserService() { } public static UserRepository createUserRepository() { - Properties props = new Properties(); - try { - props.load(new FileInputStream("application.yaml")); + Properties properties = new Properties(); + try (InputStream is = ChannelFactory.class.getClassLoader().getResourceAsStream("application.properties")) { + properties.load(is); } catch (IOException exception) { System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); } - String repositoryType = props.getProperty("repository.type", "memory"); + String repositoryType = properties.getProperty("repository.type", "memory"); return switch(repositoryType) { default -> new InMemoryUserRepository(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties new file mode 100644 index 000000000..449ef48d0 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties @@ -0,0 +1 @@ +repository.type = "memory" \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml deleted file mode 100644 index e69de29bb..000000000 From 5a680772d0080f44d262c7f8adcb5cc1ba6e480c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:20:26 +0900 Subject: [PATCH 22/38] =?UTF-8?q?refactor:=20validator=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/channel/JCFChannelService.java | 33 +++++++++--- .../service/interfaces/ChannelService.java | 8 +-- .../service/interfaces/MessageService.java | 4 +- .../service/message/JCFMessageService.java | 14 +++++- .../discodeit/config/ChannelFactory.java | 2 +- .../discodeit/domain/channel/Channel.java | 42 +++++++++++++--- .../discodeit/domain/message/Message.java | 37 +++++++++++--- .../discodeit/domain/user/BirthDate.java | 1 - .../mission/discodeit/domain/user/User.java | 17 ++++--- .../discodeit/domain/user/Username.java | 1 - .../user/validation/BirthDateValidator.java | 21 -------- .../user/validation/EmailValidator.java | 26 ---------- .../user/validation/NicknameValidator.java | 40 --------------- .../user/validation/UsernameValidator.java | 42 ---------------- .../channel/FileChannelRepository.java | 23 +++++++++ .../message/FileMessageRepository.java | 23 +++++++++ .../message/InMemoryMessageRepository.java | 23 +++++++++ .../repository/user/FileUserRepository.java | 36 +++++++++++++ .../discodeit/domain/user/BirthDateTest.java | 4 ++ .../discodeit/domain/user/EmailTest.java | 30 +++++++++++ .../discodeit/domain/user/NicknameTest.java | 35 +++++++++++++ .../discodeit/domain/user/UsernameTest.java | 42 ++++++++++++++++ .../validation/BirthDateValidatorTest.java | 18 ------- .../user/validation/EmailValidatorTest.java | 39 --------------- .../validation/NicknameValidatorTest.java | 47 ----------------- .../validation/UsernameValidatorTest.java | 50 ------------------- ...0 \353\252\250\353\215\270\353\247\201.md" | 26 ++++++++-- 27 files changed, 362 insertions(+), 322 deletions(-) delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/BirthDateTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index b1a7d1e02..7e4c20340 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -6,9 +6,11 @@ import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; +import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import java.util.UUID; @@ -16,17 +18,22 @@ public class JCFChannelService implements ChannelService { private final ChannelRepository channelRepository; + private final UserService userService; public JCFChannelService( - ChannelRepository channelRepository + ChannelRepository channelRepository, + UserService userService ) { this.channelRepository = channelRepository; + this.userService = userService; } @Override - public ChannelResponseDto create(CreateChannelRequestDto requestDto) { - ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); // TODO 예외 발생할 수 있음 -> 이넘타입에 존재하지 않을 경우 - Channel createChannel = new Channel(requestDto.name(), channelType); + public ChannelResponseDto create(UUID userId, CreateChannelRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); + // TODO 예외 발생할 수 있음 -> 이넘타입에 존재하지 않을 경우 + ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); + Channel createChannel = new Channel(requestDto.name(), channelType, foundUser); Channel savedChannel = channelRepository.save(createChannel); return ChannelResponseDto.from(savedChannel); } @@ -38,22 +45,34 @@ public Channel findOneByIdOrThrow(UUID id) { } @Override - public void changeSubject(ChangeChannelSubjectRequestDto requestDto) { + public void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + throwIsNotManager(foundUser, foundChannel); foundChannel.updateSubject(requestDto.subject()); channelRepository.save(foundChannel); } @Override - public void changeChannelName(ChangeChannelNameRequestDto requestDto) { + public void changeChannelName(UUID userId, ChangeChannelNameRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + throwIsNotManager(foundUser, foundChannel); foundChannel.updateName(requestDto.channelName()); channelRepository.save(foundChannel); } @Override - public void deleteChannel(DeleteChannelRequestDto requestDto) { + public void deleteChannel(UUID userId, DeleteChannelRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + throwIsNotManager(foundUser, foundChannel); channelRepository.deleteById(foundChannel.getId()); } + + private void throwIsNotManager(User foundUser, Channel foundChannel) { + if (!foundChannel.isManager(foundUser)) { + throw new IllegalArgumentException(); + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java index 2f09e5671..3d94a2a1a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java @@ -10,13 +10,13 @@ public interface ChannelService { - ChannelResponseDto create(CreateChannelRequestDto requestDto); + ChannelResponseDto create(UUID userId, CreateChannelRequestDto requestDto); Channel findOneByIdOrThrow(UUID uuid); - void changeSubject(ChangeChannelSubjectRequestDto requestDto); + void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto); - void changeChannelName(ChangeChannelNameRequestDto requestDto); + void changeChannelName(UUID userId, ChangeChannelNameRequestDto requestDto); - void deleteChannel(DeleteChannelRequestDto requestDto); + void deleteChannel(UUID userId, DeleteChannelRequestDto requestDto); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java index f68063458..49574fa84 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java @@ -11,9 +11,9 @@ public interface MessageService { MessageResponseDto createMessage(CreateMessageRequestDto requestDto); - void updateMessage(UpdateMessageContentRequestDto requestDto); + void updateMessage(UUID userId, UpdateMessageContentRequestDto requestDto); - void deleteMessage(DeleteMessageRequestDto requestDto); + void deleteMessage(UUID userId, DeleteMessageRequestDto requestDto); Message findOneByIdOrThrow(UUID uuid); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index c67206298..0bd4514b9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -40,15 +40,19 @@ public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { } @Override - public void updateMessage(UpdateMessageContentRequestDto requestDto) { + public void updateMessage(UUID userId, UpdateMessageContentRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); + throwIsNotSender(foundUser, foundMessage); foundMessage.updateContent(requestDto.content()); messageRepository.save(foundMessage); } @Override - public void deleteMessage(DeleteMessageRequestDto requestDto) { + public void deleteMessage(UUID userId, DeleteMessageRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); + throwIsNotSender(foundUser, foundMessage); messageRepository.deleteById(foundMessage.getId()); } @@ -57,4 +61,10 @@ public Message findOneByIdOrThrow(UUID messageId) { return messageRepository.findById(messageId) .orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); } + + private void throwIsNotSender(User foundUser, Message foundMessage) { + if (!foundMessage.isSender(foundUser)) { + throw new IllegalArgumentException(); + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java index 7efac8859..b7df9f9ab 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java @@ -12,7 +12,7 @@ public class ChannelFactory { private static final ChannelRepository CHANNEL_REPOSITORY = createChannelRepository(); - private static final ChannelService CHANNEL_SERVICE = new JCFChannelService(CHANNEL_REPOSITORY); + private static final ChannelService CHANNEL_SERVICE = new JCFChannelService(CHANNEL_REPOSITORY, UserFactory.getUserService()); public static ChannelService getChannelService() { return CHANNEL_SERVICE; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index 7fbc842eb..90595fa62 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -3,33 +3,42 @@ import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNameInvalidException; import com.sprint.mission.discodeit.domain.channel.exception.ChannelSubjectOverLengthException; +import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; +import java.io.Serial; +import java.io.Serializable; +import java.time.Instant; import java.time.LocalDateTime; import java.util.Objects; import java.util.UUID; -public class Channel { +public class Channel implements Serializable { + @Serial + private static final long serialVersionUID = 6800939087441796647L; private final static int SUBJECT_MAX_LENGTH = 1024; private final UUID id; private String name; private String subject; private ChannelType type; - private final LocalDateTime createdAt; - private LocalDateTime updatedAt; + private final Instant createdAt; + private Instant updatedAt; + private final User manager; public Channel( String name, - ChannelType type + ChannelType type, + User manager ) { validate(name); this.id = UUID.randomUUID(); this.name = name; this.subject = ""; this.type = type; - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); + createdAt = Instant.now(); + updatedAt = Instant.now(); + this.manager = manager; } public void updateSubject(String subject) { @@ -50,6 +59,10 @@ private void validate(String name) { } } + public boolean isManager(User user) { + return this.manager.equals(user); + } + public UUID getId() { return id; } @@ -65,4 +78,21 @@ public String getSubject() { public String getType() { return type.toString(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Channel channel = (Channel) o; + return Objects.equals(id, channel.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java index d1a9550a4..a479c0f46 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java @@ -4,15 +4,20 @@ import com.sprint.mission.discodeit.domain.message.exception.InvalidMessageContentException; import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; -import java.time.LocalDateTime; +import java.io.Serial; +import java.io.Serializable; +import java.time.Instant; import java.util.Objects; import java.util.UUID; -public class Message { +public class Message implements Serializable { + + @Serial + private static final long serialVersionUID = -5528993606626641717L; private final UUID id; - private final LocalDateTime createdAt; - private LocalDateTime updatedAt; + private final Instant createdAt; + private Instant updatedAt; private final User sender; private final Channel destinationChannel; private String content; @@ -23,8 +28,12 @@ public Message(User sender, Channel destinationChannel, String content) { this.sender = sender; this.destinationChannel = destinationChannel; this.content = content; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + } + + public boolean isSender(User user) { + return sender.equals(user); } public void updateContent(String content) { @@ -54,4 +63,20 @@ public UUID getDestinationChannelId() { return destinationChannel.getId(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Message message = (Message) o; + return Objects.equals(id, message.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java index 56283fb02..a2dc412ca 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java @@ -1,7 +1,6 @@ package com.sprint.mission.discodeit.domain.user; import com.sprint.mission.discodeit.domain.user.exception.BirthDateInvalidException; -import com.sprint.mission.discodeit.domain.user.validation.BirthDateValidator; import com.sprint.mission.discodeit.global.error.ErrorCode; import java.time.LocalDate; import java.util.Objects; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java index 806b11930..4825d73e9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -1,12 +1,17 @@ package com.sprint.mission.discodeit.domain.user; import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; -import java.time.LocalDateTime; +import java.io.Serial; +import java.io.Serializable; +import java.time.Instant; import java.util.Objects; import java.util.UUID; import lombok.Builder; -public class User { +public class User implements Serializable { + + @Serial + private static final long serialVersionUID = 1284229838745272170L; private final UUID id; private final Nickname nickname; @@ -14,8 +19,8 @@ public class User { private final Email email; private final Password password; private final BirthDate birthDate; - private final LocalDateTime createdAt; - private final LocalDateTime updatedAt; + private final Instant createdAt; + private final Instant updatedAt; private final EmailSubscriptionStatus emailSubscriptionStatus; @Builder @@ -28,8 +33,8 @@ public User( EmailSubscriptionStatus emailSubscriptionStatus ) { this.id = UUID.randomUUID(); - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); this.nickname = nickname; this.username = username; this.email = email; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java index d6dab6860..8bebe8eef 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java @@ -1,7 +1,6 @@ package com.sprint.mission.discodeit.domain.user; import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; -import com.sprint.mission.discodeit.domain.user.validation.UsernameValidator; import com.sprint.mission.discodeit.global.error.ErrorCode; import java.util.Objects; import java.util.Set; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java deleted file mode 100644 index cf8469d30..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidator.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import com.sprint.mission.discodeit.domain.user.exception.BirthDateInvalidException; -import com.sprint.mission.discodeit.global.error.ErrorCode; -import java.time.LocalDate; -import java.util.Objects; - -public class BirthDateValidator { - private static final int MIN_AGE_RESTRICT = 13; - - public static void validate(LocalDate birthDate) { - if (Objects.isNull(birthDate)) { - throw new BirthDateInvalidException(ErrorCode.BIRTHDATE_REQUIRED, ""); - } - - LocalDate minimumAllowedYear = LocalDate.now().minusYears(MIN_AGE_RESTRICT); - if (!birthDate.isBefore(minimumAllowedYear)) { - throw new BirthDateInvalidException(ErrorCode.UNDERAGE_SIGNUP_REGISTERED, birthDate.toString()); - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java deleted file mode 100644 index 53844f0e6..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidator.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import com.sprint.mission.discodeit.domain.user.exception.EmailInvalidException; -import com.sprint.mission.discodeit.global.error.ErrorCode; -import java.util.Objects; -import java.util.regex.Pattern; - -public class EmailValidator { - - private final static String EMAIL_REGEX = - "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z]{2,7})+$"; - - private final static Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX); - - - public static void valid(String email) { - if (Objects.isNull(email) || email.isBlank()) { - throw new EmailInvalidException(ErrorCode.EMAIL_REQUIRED, ""); - } - - if (!EMAIL_PATTERN.matcher(email).matches()) { - throw new EmailInvalidException(ErrorCode.INVALID_EMAIL_FORMAT, email); - } - - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java deleted file mode 100644 index bdc8b656d..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import com.sprint.mission.discodeit.domain.user.exception.NickNameInvalidException; -import com.sprint.mission.discodeit.global.error.ErrorCode; -import java.util.Objects; -import java.util.Set; -import java.util.regex.Pattern; - -public class NicknameValidator { - private static final int MAX_LENGTH = 32; - private static final Set FORBIDDEN_WORDS = Set.of( - "admin", "moderator", "discord", "system", "root", "bot", "mod", - "운영자", "관리자", "봇" - ); - - private static final String VALID_NICK_NAME_REGEX = - "^(?!.*\\s)[\\p{L}\\p{N}\\p{P}\\p{S}\\p{So}]{1,32}$"; - private static final Pattern VALID_NICK_NAME_PATTERN = Pattern.compile(VALID_NICK_NAME_REGEX); - - public static void validate(String value) { - if (Objects.isNull(value) || value.isBlank()) { - throw new NickNameInvalidException(ErrorCode.NICKNAME_REQUIRED, ""); - } - - if (value.length() > MAX_LENGTH) { - throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_LENGTH, value); - } - - if (!VALID_NICK_NAME_PATTERN.matcher(value).matches()) { - throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_FORMAT, value); - } - - String lowerCase = value.toLowerCase(); - for (String word : FORBIDDEN_WORDS) { - if (lowerCase.contains(word)) { - throw new NickNameInvalidException(ErrorCode.INVALID_NICKNAME_FORMAT, value); - } - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java deleted file mode 100644 index 679a8b9b0..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; -import com.sprint.mission.discodeit.global.error.ErrorCode; -import java.util.Objects; -import java.util.Set; -import java.util.regex.Pattern; - -public class UsernameValidator { - private static final int MIN_LENGTH = 2; - private static final int MAX_LENGTH = 32; - private static final String VALID_USER_NAME_REGEX = - "^(?!.*\\.\\.)(?![.])(?!.*[.]$)[a-zA-Z0-9_.]{2,32}$"; - private static final Pattern VALID_USER_NAME_PATTERN = Pattern.compile(VALID_USER_NAME_REGEX); - private static final Set FORBIDDEN_WORD = - Set.of( - "admin", "moderator", "discord", "system", "root", "bot", "mod", - "운영자", "관리자", "봇" - ); - - - public static void validate(final String username) { - if (Objects.isNull(username) || username.isBlank()) { - throw new UserNameInvalidException(ErrorCode.USERNAME_REQUIRED, ""); - } - - if (username.length() > MAX_LENGTH || username.length() < MIN_LENGTH) { - throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_LENGTH, username); - } - - if (!VALID_USER_NAME_PATTERN.matcher(username).matches()) { - throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); - } - - String lowerUsername = username.toLowerCase(); - for (String word : FORBIDDEN_WORD) { - if (lowerUsername.contains(word)) { - throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); - } - } - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java new file mode 100644 index 000000000..ee79d46f7 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.repository.channel; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import java.util.Optional; +import java.util.UUID; + +public class FileChannelRepository implements ChannelRepository { + @Override + public Channel save(Channel channel) { + return null; + } + + @Override + public Optional findOneById(UUID uuid) { + return Optional.empty(); + } + + @Override + public void deleteById(UUID uuid) { + + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java new file mode 100644 index 000000000..83a328d74 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.repository.message; + +import com.sprint.mission.discodeit.domain.message.Message; +import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.util.Optional; +import java.util.UUID; + +public class FileMessageRepository implements MessageRepository { + @Override + public Message save(Message message) { + return null; + } + + @Override + public Optional findById(UUID uuid) { + return Optional.empty(); + } + + @Override + public void deleteById(UUID uuid) { + + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java new file mode 100644 index 000000000..20324e84a --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.repository.message; + +import com.sprint.mission.discodeit.domain.message.Message; +import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.util.Optional; +import java.util.UUID; + +public class InMemoryMessageRepository implements MessageRepository { + @Override + public Message save(Message message) { + return null; + } + + @Override + public Optional findById(UUID uuid) { + return Optional.empty(); + } + + @Override + public void deleteById(UUID uuid) { + + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java new file mode 100644 index 000000000..8ef29fb52 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java @@ -0,0 +1,36 @@ +package com.sprint.mission.discodeit.repository.user; + +import com.sprint.mission.discodeit.domain.user.Email; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.user.Username; +import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.util.Optional; +import java.util.UUID; + +public class FileUserRepository implements UserRepository { + + @Override + public User save(User user) { + return null; + } + + @Override + public Optional findOneById(UUID id) { + return Optional.empty(); + } + + @Override + public Optional findOneByEmail(Email email) { + return Optional.empty(); + } + + @Override + public boolean isExistByEmail(Email email) { + return false; + } + + @Override + public boolean isExistByUsername(Username username) { + return false; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/BirthDateTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/BirthDateTest.java new file mode 100644 index 000000000..953de818d --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/BirthDateTest.java @@ -0,0 +1,4 @@ +package com.sprint.mission.discodeit.domain.user; + +class BirthDateTest { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java index 1ef1ca6ff..352a39b5d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java @@ -1,7 +1,11 @@ package com.sprint.mission.discodeit.domain.user; +import static org.assertj.core.api.Assertions.assertThat; + +import com.sprint.mission.discodeit.domain.user.exception.EmailInvalidException; import org.assertj.core.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; class EmailTest { @@ -29,4 +33,30 @@ class EmailTest { // then Assertions.assertThat(createdEmail.getValue()).isEqualTo(email); } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { + "abc", // 일반 문자열 + "abc@", // '@'만 포함 + "@example.com", // 로컬 부분 없음 + "abc@example", // 최상위 도메인 없음 + "abc@.com", // 도메인 부분이 비정상적 + "abc@com.", // 도메인 형식 오류 + "abc@@example.com", // '@' 중복 + "abc example.com", // 공백 포함 + "abc@ex@ample.com", // 여러 개의 '@' + "abc@example..com", // 연속된 '.' 포함 + "abc@example.c", // 너무 짧은 TLD + "abc@example#com", // 특수 문자 포함 + "abc@.com", // 도메인 앞부분 없음 + "abc@exam_ple.com", // 언더스코어 포함 (일반적으로 허용되지 않음) + }) + void 이메일_생성_Invalid값_에러throw(String email) { + //given + // when + Throwable throwable = Assertions.catchThrowable(() -> new Email(email)); + // then + assertThat(throwable).isInstanceOf(EmailInvalidException.class); + } } \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java index 3dca6f1de..f984f9a29 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/NicknameTest.java @@ -1,8 +1,13 @@ package com.sprint.mission.discodeit.domain.user; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import com.sprint.mission.discodeit.domain.user.exception.NickNameInvalidException; +import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; class NicknameTest { @@ -33,4 +38,34 @@ class NicknameTest { assertThat(createNickname.getValue()).isEqualTo(nickname); } + @ParameterizedTest + @NullAndEmptySource + @MethodSource("nicknameOverLengthProvider") + @ValueSource(strings = { + "admin", // 금지된 단어 + "moderator", // 금지된 단어 + "discord123", // 금지된 단어 포함 + "superadmin", // 금지된 단어 포함 + "mod🚀", // 금지된 단어 포함 + 이모지 (이모지는 가능하지만 금지어 포함이라 제한) + "hello admin!", // 금지된 단어 포함 + "AdminUser", // 금지된 단어 포함 (대소문자 무시) + "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) + "papago good", + "root_user", // 금지된 단어 포함 (관리자 권한 관련) + "system_mod", // 금지된 단어 포함 + "운영자", // 한글 운영자 금지 + "테스트관리자", // 한글 관리자 관련 단어 포함 + "봇", // 봇 계정 금지 + }) + void 유저닉네임_제한되는_입력값_에러검증throw(String nickname) { + //given + // when + Throwable catchThrowable = catchThrowable(() -> new Nickname(nickname)); + // then + assertThat(catchThrowable).isInstanceOf(NickNameInvalidException.class); + } + + static Stream nicknameOverLengthProvider() { + return Stream.of("a".repeat(33)); + } } \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java index 90ce0115b..41bb6f080 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/UsernameTest.java @@ -2,7 +2,12 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; class UsernameTest { @@ -27,4 +32,41 @@ class UsernameTest { assertThat(createUsername.getValue()).isEqualToIgnoringCase(username); } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = { + "a", // 너무 짧음 (최소 2자) + "abcdefghijklmnopqrstuvwxyzabcdefghi", // 너무 김 (32자 초과, 33자) + "user name", // 공백 포함 (허용되지 않음) + "🔥coolgamer", // 이모지 포함 (정규식이 허용하지 않음) + "user!name", // 특수문자 포함 (! 허용되지 않음) + "....hidden....", // 연속된 마침표 (허용되지 않음) + ".username", // 마침표로 시작 (허용되지 않음) + "username.", // 마침표로 끝남 (허용되지 않음) + "admin", // 금지된 단어 포함 + "moderator", // 금지된 단어 포함 + "discord123", // 금지된 단어 포함 + "superadmin", // 금지된 단어 포함 + "mod🚀", // 금지된 단어 포함 + 이모지 + "hello admin!", // 금지된 단어 포함 + "AdminUser", // 금지된 단어 포함 (대소문자 무시) + "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) + "root_user", // 금지된 단어 포함 (관리자 권한 관련) + "system_mod", // 금지된 단어 포함 + "운영자", // 한글 운영자 금지 + "테스트관리자", // 한글 관리자 관련 단어 포함 + "봇", // 봇 계정 금지 + }) + @MethodSource("usernameOverLengthProvider") + void 유저_이름_제한_검증_에러throw(String username) { + //given + // when + Throwable catchThrowable = Assertions.catchThrowable(() -> new Username(username)); + // then + Assertions.assertThat(catchThrowable).isInstanceOf(UserNameInvalidException.class); + } + + static Stream usernameOverLengthProvider() { + return Stream.of("a".repeat(33)); + } } \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java deleted file mode 100644 index 6242d8416..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/BirthDateValidatorTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -class BirthDateValidatorTest { - - @Test - void 나이_13세_미만_생성검증_에러throw() { - //given - - // when - - // then - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java deleted file mode 100644 index 63d1a40ed..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/EmailValidatorTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sprint.mission.discodeit.domain.user.exception.EmailInvalidException; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -class EmailValidatorTest { - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = { - "abc", // 일반 문자열 - "abc@", // '@'만 포함 - "@example.com", // 로컬 부분 없음 - "abc@example", // 최상위 도메인 없음 - "abc@.com", // 도메인 부분이 비정상적 - "abc@com.", // 도메인 형식 오류 - "abc@@example.com", // '@' 중복 - "abc example.com", // 공백 포함 - "abc@ex@ample.com", // 여러 개의 '@' - "abc@example..com", // 연속된 '.' 포함 - "abc@example.c", // 너무 짧은 TLD - "abc@example#com", // 특수 문자 포함 - "abc@.com", // 도메인 앞부분 없음 - "abc@exam_ple.com", // 언더스코어 포함 (일반적으로 허용되지 않음) - }) - void 이메일_생성_Invalid값_에러throw(String email) { - //given - // when - Throwable throwable = Assertions.catchThrowable(() -> EmailValidator.valid(email)); - // then - assertThat(throwable).isInstanceOf(EmailInvalidException.class); - } - -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java deleted file mode 100644 index afbd5b4a6..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/NicknameValidatorTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.junit.jupiter.api.Assertions.*; - -import com.sprint.mission.discodeit.domain.user.Nickname; -import com.sprint.mission.discodeit.domain.user.exception.NickNameInvalidException; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -class NicknameValidatorTest { - - @ParameterizedTest - @NullAndEmptySource - @MethodSource("nicknameOverLengthProvider") - @ValueSource(strings = { - "admin", // 금지된 단어 - "moderator", // 금지된 단어 - "discord123", // 금지된 단어 포함 - "superadmin", // 금지된 단어 포함 - "mod🚀", // 금지된 단어 포함 + 이모지 (이모지는 가능하지만 금지어 포함이라 제한) - "hello admin!", // 금지된 단어 포함 - "AdminUser", // 금지된 단어 포함 (대소문자 무시) - "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) - "papago good", - "root_user", // 금지된 단어 포함 (관리자 권한 관련) - "system_mod", // 금지된 단어 포함 - "운영자", // 한글 운영자 금지 - "테스트관리자", // 한글 관리자 관련 단어 포함 - "봇", // 봇 계정 금지 - }) - void 유저닉네임_제한되는_입력값_에러검증throw(String nickname) { - //given - // when - Throwable catchThrowable = catchThrowable(() -> new Nickname(nickname)); - // then - assertThat(catchThrowable).isInstanceOf(NickNameInvalidException.class); - } - - static Stream nicknameOverLengthProvider() { - return Stream.of("a".repeat(33)); - } -} \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java deleted file mode 100644 index 975c06466..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/validation/UsernameValidatorTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sprint.mission.discodeit.domain.user.validation; - -import com.sprint.mission.discodeit.domain.user.exception.UserNameInvalidException; -import java.util.stream.Stream; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; - -class UsernameValidatorTest { - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = { - "a", // 너무 짧음 (최소 2자) - "abcdefghijklmnopqrstuvwxyzabcdefghi", // 너무 김 (32자 초과, 33자) - "user name", // 공백 포함 (허용되지 않음) - "🔥coolgamer", // 이모지 포함 (정규식이 허용하지 않음) - "user!name", // 특수문자 포함 (! 허용되지 않음) - "....hidden....", // 연속된 마침표 (허용되지 않음) - ".username", // 마침표로 시작 (허용되지 않음) - "username.", // 마침표로 끝남 (허용되지 않음) - "admin", // 금지된 단어 포함 - "moderator", // 금지된 단어 포함 - "discord123", // 금지된 단어 포함 - "superadmin", // 금지된 단어 포함 - "mod🚀", // 금지된 단어 포함 + 이모지 - "hello admin!", // 금지된 단어 포함 - "AdminUser", // 금지된 단어 포함 (대소문자 무시) - "DISCORD🔥", // 금지된 단어 포함 (대소문자 무시) - "root_user", // 금지된 단어 포함 (관리자 권한 관련) - "system_mod", // 금지된 단어 포함 - "운영자", // 한글 운영자 금지 - "테스트관리자", // 한글 관리자 관련 단어 포함 - "봇", // 봇 계정 금지 - }) - @MethodSource("usernameOverLengthProvider") - void 유저_이름_제한_검증_에러throw(String username) { - //given - // when - Throwable catchThrowable = Assertions.catchThrowable(() -> UsernameValidator.validate(username)); - // then - Assertions.assertThat(catchThrowable).isInstanceOf(UserNameInvalidException.class); - } - - static Stream usernameOverLengthProvider() { - return Stream.of("a".repeat(33)); - } -} \ No newline at end of file diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 96928068b..492fe1211 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -43,14 +43,16 @@ ## 채널 - 새로운 채널 생성 - - `채널이름`, `채널 유형`을 입력받는다. + - `채널이름`, `채널 유형`, `채널 생성 유저`를 입력받는다. - 텍스트 채널 유형 - 음성, 영상, 화면 유형 - 채널 주제 (생성 시에는 입력받지 않음, 1024 글자 제한) - 채널 삭제 + - 채널 매니저가 아닐 경우 에러 - 채널 정보 수정 - - 이름 수정 - - 채널 주제 수정 1024 글자제한 + - 채널 매니저가 아닐 경우 에러 + - 이름 수정 + - 채널 주제 수정 1024 글자제한 ## 메세지 @@ -58,6 +60,24 @@ - `메시지 id`, `메시지 생성, 수정시간`, `메시지 내용`, `보낸사람`, `목적지 채널` - 메시지 내용은 비어있을 수 없다. - 메시지 내용 수정 + - 발신자가 아니라면 에러 - 메시지 삭제 + - 발진자가 아니라면 에러 + + +## ReadStatus + - 사용자가 채널 별 마지막으로 메시지를 읽은 시간을 표현하는 도메인 모델입니다. 사용자별 각 채널에 읽지 않은 메시지를 확인하기 위해 활용합니다. + - `id`, `createdAt`, `updatedAt` + +## UserStatus +- 사용자 별 마지막으로 확인된 접속 시간을 표현하는 도메인 모델입니다. 사용자의 온라인 상태를 확인하기 위해 활용합니다. +- 마지막 접속 시간을 기준으로 현재 로그인한 유저로 판단할 수 있는 메소드를 정의하세요. + - 마지막 접속 시간이 현재 시간으로부터 5분 이내이면 현재 접속 중인 유저로 간주합니다. +- `id`, `createdAt`, `updatedAt` + +## BinaryContent +- 이미지, 파일 등 바이너리 데이터를 표현하는 도메인 모델입니다. 사용자의 프로필 이미지, 메시지에 첨부된 파일을 저장하기 위해 활용합니다. +- 수정 불가능한 도메인 모델로 간주합니다. 따라서 updatedAt 필드는 정의하지 않습니다. +- User, Message 도메인 모델과의 의존 관계 방향성을 잘 고려하여 id 참조 필드를 추가하세요. -- newLine for git From 08dd9f041ef7640c82fcc2931a644772319bfa55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:37:56 +0900 Subject: [PATCH 23/38] =?UTF-8?q?feat:=20readStatus=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadStatusUpdateRequestDto.java | 8 +++ .../service/readstatus/ReadStatusService.java | 41 ++++++++++++++ .../domain/readStatus/ReadStatus.java | 49 ++++++++++++++++ .../ReadStatusInMemoryRepository.java | 56 +++++++++++++++++++ .../interfaces/ReadStatusRepository.java | 13 +++++ ...0 \353\252\250\353\215\270\353\247\201.md" | 5 ++ 6 files changed, 172 insertions(+) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/readstatus/ReadStatusUpdateRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/readStatus/ReadStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/readstatus/ReadStatusUpdateRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/readstatus/ReadStatusUpdateRequestDto.java new file mode 100644 index 000000000..b2060bb36 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/readstatus/ReadStatusUpdateRequestDto.java @@ -0,0 +1,8 @@ +package com.sprint.mission.discodeit.application.dto.readstatus; + +import java.util.UUID; + +public record ReadStatusUpdateRequestDto( + UUID channelId +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java new file mode 100644 index 000000000..33b390387 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java @@ -0,0 +1,41 @@ +package com.sprint.mission.discodeit.application.service.readstatus; + +import com.sprint.mission.discodeit.application.dto.readstatus.ReadStatusUpdateRequestDto; +import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.UserService; +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.repository.readstatus.interfaces.ReadStatusRepository; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +public class ReadStatusService { + + private final ReadStatusRepository readStatusRepository; + private final UserService userService; + private final ChannelService channelService; + + public ReadStatusService( + ReadStatusRepository readStatusRepository, + UserService userService, + ChannelService channelService + ) { + this.readStatusRepository = readStatusRepository; + this.userService = userService; + this.channelService = channelService; + } + + public void updateLastReadTime(UUID userId, ReadStatusUpdateRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); + Channel foundChannel = channelService.findOneByIdOrThrow(requestDto.channelId()); + ReadStatus readStatus = findOneByUserIdAndChannelId(foundUser, foundChannel).orElseGet(() -> new ReadStatus(foundUser, foundChannel)); + readStatus.updateLastReadAt(Instant.now()); + readStatusRepository.save(readStatus); + } + + public Optional findOneByUserIdAndChannelId(User foundUser, Channel foundChannel) { + return readStatusRepository.findOneByUserIdAndChannelId(foundUser, foundChannel); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/readStatus/ReadStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/readStatus/ReadStatus.java new file mode 100644 index 000000000..b72f3f4e8 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/readStatus/ReadStatus.java @@ -0,0 +1,49 @@ +package com.sprint.mission.discodeit.domain.readStatus; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.user.User; +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +public class ReadStatus { + + private final UUID id; + private final User user; + private final Channel channel; + private final Instant createdAt; + private Instant updatedAt; + private Instant lastReadAt; + + public ReadStatus(User user, Channel channel) { + validate(user, channel); + id = UUID.randomUUID(); + this.user = user; + this.channel = channel; + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + this.lastReadAt = Instant.now(); + } + + public void updateLastReadAt(Instant lastReadAt) { + this.lastReadAt = lastReadAt; + this.updatedAt = lastReadAt; + } + + private void validate(User user, Channel channel) { + if (Objects.isNull(user)) { + throw new IllegalArgumentException("user is null"); + } + if (Objects.isNull(channel)) { + throw new IllegalArgumentException("channel is null"); + } + } + + public User getUser() { + return user; + } + + public Channel getChannel() { + return channel; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java new file mode 100644 index 000000000..41a252029 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java @@ -0,0 +1,56 @@ +package com.sprint.mission.discodeit.repository.readstatus; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.repository.readstatus.interfaces.ReadStatusRepository; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +public class ReadStatusInMemoryRepository implements ReadStatusRepository { + + private final Map readStatusMap = new HashMap<>(); + + @Override + public ReadStatus save(ReadStatus readStatus) { + ReadStatusKey readStatusKey = new ReadStatusKey(readStatus.getUser(), readStatus.getChannel()); + readStatusMap.put(readStatusKey, readStatus); + return readStatus; + } + + @Override + public Optional findOneByUserIdAndChannelId(User user, Channel channel) { + ReadStatusKey readStatusKey = new ReadStatusKey(user, channel); + return Optional.ofNullable(readStatusMap.get(readStatusKey)); + } + + private static class ReadStatusKey { + private final UUID userId; + private final UUID channelId; + + public ReadStatusKey(User user, Channel channel) { + this.userId = user.getId(); + this.channelId = channel.getId(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReadStatusKey that = (ReadStatusKey) o; + return Objects.equals(userId, that.userId) && Objects.equals(channelId, that.channelId); + } + + @Override + public int hashCode() { + return Objects.hash(userId, channelId); + } + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java new file mode 100644 index 000000000..ff07a73ae --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.repository.readstatus.interfaces; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; +import com.sprint.mission.discodeit.domain.user.User; +import java.util.Optional; + +public interface ReadStatusRepository { + + ReadStatus save(ReadStatus readStatus); + + Optional findOneByUserIdAndChannelId(User user, Channel channel); +} diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 492fe1211..10c59d54c 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -69,6 +69,11 @@ - 사용자가 채널 별 마지막으로 메시지를 읽은 시간을 표현하는 도메인 모델입니다. 사용자별 각 채널에 읽지 않은 메시지를 확인하기 위해 활용합니다. - `id`, `createdAt`, `updatedAt` + - 유저가 채널별 읽은 시간을 기록한다. + - 유저가 채널에 접속한 시간을 업데이트한다. + - 접속한 적이 없는 경우에는 새로운 객체를 생성하여 저장한다. + - 접속한 적이 있는 경우에는 기존 객체의 접속 시간을 업데이트한다. + ## UserStatus - 사용자 별 마지막으로 확인된 접속 시간을 표현하는 도메인 모델입니다. 사용자의 온라인 상태를 확인하기 위해 활용합니다. - 마지막 접속 시간을 기준으로 현재 로그인한 유저로 판단할 수 있는 메소드를 정의하세요. From eb5079a99927811f5523e6c6c23a9f5a5821f731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:21:05 +0900 Subject: [PATCH 24/38] =?UTF-8?q?feat:=20userstatus=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/user/converter/UserConverter.java | 2 +- .../service/userstatus/UserStatusService.java | 39 +++++++++++++++ .../domain/userstatus/UserStatus.java | 47 +++++++++++++++++++ .../domain/userstatus/enums/OnlineStatus.java | 6 +++ .../discodeit/global/error/ErrorCode.java | 1 - .../UserStatusInMemoryRepository.java | 25 ++++++++++ .../interfaces/UserStatusRepository.java | 12 +++++ .../discodeit/domain/user/EmailTest.java | 2 +- 8 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/enums/OnlineStatus.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java index fc0c7f045..79c3affb8 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java @@ -21,7 +21,7 @@ public User toUser(joinUserRequestDto requestDto) { .email(new Email(requestDto.email())) .password(new Password(requestDto.password())) .birthDate(new BirthDate(requestDto.birthDate())) - .emailSubscriptionStatus(EmailSubscriptionStatus.UNSUBSCRIBED) // TODO 이넘 타입 dto에서 어떻게 받아서 넘겨야하나. + .emailSubscriptionStatus(EmailSubscriptionStatus.UNSUBSCRIBED) .build(); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java new file mode 100644 index 000000000..c53f109e7 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java @@ -0,0 +1,39 @@ +package com.sprint.mission.discodeit.application.service.userstatus; + +import com.sprint.mission.discodeit.application.service.interfaces.UserService; +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.userstatus.UserStatus; +import com.sprint.mission.discodeit.domain.userstatus.enums.OnlineStatus; +import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; +import java.util.UUID; + +public class UserStatusService { + + private final UserStatusRepository userStatusRepository; + private final UserService userService; + + public UserStatusService( + UserStatusRepository userStatusRepository, + UserService userService + ) { + this.userStatusRepository = userStatusRepository; + this.userService = userService; + } + + public UserStatus findOneByUser(User user) { + return userStatusRepository.findByUser(user).orElse(new UserStatus(user)); + } + + public void updateLastAccessTime(UUID userId) { + User foundUser = userService.findOneByIdOrThrow(userId); + UserStatus foundUserStatus = findOneByUser(foundUser); + foundUserStatus.updateLastAccessedAt(); + userStatusRepository.save(foundUserStatus); + } + + public OnlineStatus getUserStatus(UUID userId) { + User foundUser = userService.findOneByIdOrThrow(userId); + UserStatus foundUserStatus = findOneByUser(foundUser); + return foundUserStatus.getOnlineStatus(); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java new file mode 100644 index 000000000..49fbf6878 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java @@ -0,0 +1,47 @@ +package com.sprint.mission.discodeit.domain.userstatus; + +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.userstatus.enums.OnlineStatus; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.UUID; + +public class UserStatus { + + private final UUID id; + private final User user; + private final Instant createdAt; + private Instant updatedAt; + private Instant lastAccessedAt; + + public UserStatus(User user) { + validate(user); + this.id = UUID.randomUUID(); + this.user = user; + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + this.lastAccessedAt = Instant.now(); + } + + private void validate(User user) { + if (Objects.isNull(user)) { + throw new IllegalArgumentException("user is null"); + } + } + + public void updateLastAccessedAt() { + this.lastAccessedAt = Instant.now(); + } + + public OnlineStatus getOnlineStatus() { + if (lastAccessedAt.isAfter(Instant.now().minus(5, ChronoUnit.MINUTES))) { + return OnlineStatus.ONLINE; + } + return OnlineStatus.OFFLINE; + } + + public User getUser() { + return user; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/enums/OnlineStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/enums/OnlineStatus.java new file mode 100644 index 000000000..a1b2590a0 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/enums/OnlineStatus.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.domain.userstatus.enums; + +public enum OnlineStatus { + ONLINE, + OFFLINE +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index 94e5dedaa..9b88e3409 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -49,5 +49,4 @@ public int getStatus() { public String getDescription() { return description; } - } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java new file mode 100644 index 000000000..4f9e72b9e --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java @@ -0,0 +1,25 @@ +package com.sprint.mission.discodeit.repository.userstatus; + +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.userstatus.UserStatus; +import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class UserStatusInMemoryRepository implements UserStatusRepository { + + private final Map userStatuses = new HashMap<>(); + + @Override + public UserStatus save(UserStatus userStatus) { + User user = userStatus.getUser(); + userStatuses.put(user, userStatus); + return userStatus; + } + + @Override + public Optional findByUser(User user) { + return Optional.ofNullable(userStatuses.get(user)); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java new file mode 100644 index 000000000..d66ef1b09 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java @@ -0,0 +1,12 @@ +package com.sprint.mission.discodeit.repository.userstatus.interfaces; + +import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.domain.userstatus.UserStatus; +import java.util.Optional; + +public interface UserStatusRepository { + + UserStatus save(UserStatus userStatus); + + Optional findByUser(User user); +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java index 352a39b5d..a5c432ce4 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/domain/user/EmailTest.java @@ -52,7 +52,7 @@ class EmailTest { "abc@.com", // 도메인 앞부분 없음 "abc@exam_ple.com", // 언더스코어 포함 (일반적으로 허용되지 않음) }) - void 이메일_생성_Invalid값_에러throw(String email) { + void 이메일_생성_Invalid_값_에러throw(String email) { //given // when Throwable throwable = Assertions.catchThrowable(() -> new Email(email)); From c6ad93ebdb9a725af76e3a4bde8ade2482b93025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:52:30 +0900 Subject: [PATCH 25/38] =?UTF-8?q?feat:=20user=20profile=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discodeit/domain/binarycontent/Asset.java | 4 +++ .../domain/binarycontent/BinaryContent.java | 35 +++++++++++++++++++ .../binarycontent/enums/BinaryType.java | 4 +++ .../discodeit/domain/user/ProfileImage.java | 4 +++ .../domain/userstatus/UserStatus.java | 2 -- .../BinaryInMemoryRepository.java | 14 ++++++++ .../interfaces/BinaryRepository.java | 9 +++++ ...0 \353\252\250\353\215\270\353\247\201.md" | 2 ++ 8 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/Asset.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/BinaryContent.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/enums/BinaryType.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/ProfileImage.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/BinaryInMemoryRepository.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/interfaces/BinaryRepository.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/Asset.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/Asset.java new file mode 100644 index 000000000..7b67d5136 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/Asset.java @@ -0,0 +1,4 @@ +package com.sprint.mission.discodeit.domain.binarycontent; + +public class Asset { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/BinaryContent.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/BinaryContent.java new file mode 100644 index 000000000..3e85006ec --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/BinaryContent.java @@ -0,0 +1,35 @@ +package com.sprint.mission.discodeit.domain.binarycontent; + +import com.sprint.mission.discodeit.domain.binarycontent.enums.BinaryType; +import com.sprint.mission.discodeit.domain.message.Message; +import com.sprint.mission.discodeit.domain.user.User; +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +public class BinaryContent { + + private final UUID id; + private final Instant createdAt; + private final User user; + private final Message message; + private final Asset asset; + private final BinaryType binaryType; + + public BinaryContent(User user, Message message, Asset asset, BinaryType binaryType) { + validate(user, message, binaryType); + this.id = UUID.randomUUID(); + this.createdAt = Instant.now(); + this.user = user; + this.message = message; + this.asset = asset; + this.binaryType = binaryType; + } + + private void validate(User user, Message message, BinaryType binaryType) { + if (Objects.isNull(user) || Objects.isNull(message) || Objects.isNull(asset)) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/enums/BinaryType.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/enums/BinaryType.java new file mode 100644 index 000000000..f831cfab4 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/binarycontent/enums/BinaryType.java @@ -0,0 +1,4 @@ +package com.sprint.mission.discodeit.domain.binarycontent.enums; + +public enum BinaryType { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/ProfileImage.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/ProfileImage.java new file mode 100644 index 000000000..2b4eedfb5 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/ProfileImage.java @@ -0,0 +1,4 @@ +package com.sprint.mission.discodeit.domain.user; + +public class ProfileImage { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java index 49fbf6878..be26baf53 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java @@ -12,7 +12,6 @@ public class UserStatus { private final UUID id; private final User user; private final Instant createdAt; - private Instant updatedAt; private Instant lastAccessedAt; public UserStatus(User user) { @@ -20,7 +19,6 @@ public UserStatus(User user) { this.id = UUID.randomUUID(); this.user = user; this.createdAt = Instant.now(); - this.updatedAt = Instant.now(); this.lastAccessedAt = Instant.now(); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/BinaryInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/BinaryInMemoryRepository.java new file mode 100644 index 000000000..98e555287 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/BinaryInMemoryRepository.java @@ -0,0 +1,14 @@ +package com.sprint.mission.discodeit.repository.binarycontent; + +import com.sprint.mission.discodeit.domain.binarycontent.BinaryContent; +import com.sprint.mission.discodeit.repository.binarycontent.interfaces.BinaryRepository; + +public class BinaryInMemoryRepository implements BinaryRepository { + + @Override + public BinaryContent save(BinaryContent binaryContent) { + + return binaryContent; + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/interfaces/BinaryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/interfaces/BinaryRepository.java new file mode 100644 index 000000000..f4dc32c84 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/binarycontent/interfaces/BinaryRepository.java @@ -0,0 +1,9 @@ +package com.sprint.mission.discodeit.repository.binarycontent.interfaces; + +import com.sprint.mission.discodeit.domain.binarycontent.BinaryContent; + +public interface BinaryRepository { + + BinaryContent save(BinaryContent binaryContent); + +} diff --git "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" index 10c59d54c..397dfebe4 100644 --- "a/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" +++ "b/codeit-bootcamp-spring/1-sprint-mission/study/\353\217\204\353\251\224\354\235\270 \353\252\250\353\215\270\353\247\201.md" @@ -26,6 +26,8 @@ - 사용하고 있는 유저이름인지 검증한다. - 비밀번호를 저장할 때 암호화를 진행한다. - [jBCrypt](https://www.mindrot.org/projects/jBCrypt/) + - 회원 프로필 사진을 저장한다. + - 파일을 입력받아 저장한다. - 로그인 - `이메일`과 `비밀번호`를 입력받는다. From d08f484abcd878b953c2b86cdc876c58091761a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:04:35 +0900 Subject: [PATCH 26/38] =?UTF-8?q?feat:=20user,=20channel=20service=20?= =?UTF-8?q?=EA=B3=A0=EB=8F=84=ED=99=94=20=EA=B8=B0=EB=8A=A5=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/channel/CreateChannelRequestDto.java | 3 +- .../dto/channel/InviteChannelRequestDto.java | 8 ++++ .../application/dto/user/UserResponseDto.java | 5 ++- .../service/channel/JCFChannelService.java | 45 ++++++++++++++++--- .../service/interfaces/ChannelService.java | 9 +++- .../service/interfaces/UserService.java | 4 ++ .../service/user/JCFUserService.java | 29 +++++++++++- .../service/user/converter/UserConverter.java | 24 ++-------- .../service/userstatus/UserStatusService.java | 12 ++++- .../discodeit/domain/channel/Channel.java | 17 ++++++- .../domain/channel/ParticipatedUser.java | 15 +++++++ .../channel/enums/ChannelVisibility.java | 6 +++ .../discodeit/global/error/ErrorResponse.java | 44 ++++++++++++++++++ .../global/error/GlobalException.java | 39 ++++++++++++++++ .../error/exception/BusinessException.java | 4 ++ .../repository/user/FileUserRepository.java | 11 +++++ .../user/InMemoryUserRepository.java | 13 ++++++ .../user/interfaces/UserRepository.java | 5 +++ .../UserStatusInMemoryRepository.java | 7 +++ .../interfaces/UserStatusRepository.java | 2 + .../fake/repository/FakeUserRepository.java | 13 ++++++ 21 files changed, 283 insertions(+), 32 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/InviteChannelRequestDto.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelVisibility.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorResponse.java create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java index 2315ce67c..eb690f48e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java @@ -1,9 +1,10 @@ package com.sprint.mission.discodeit.application.dto.channel; +import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import jakarta.validation.constraints.NotBlank; public record CreateChannelRequestDto( @NotBlank String name, - @NotBlank String channelType + @NotBlank ChannelType channelType ) { } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/InviteChannelRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/InviteChannelRequestDto.java new file mode 100644 index 000000000..e9aea23dd --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/InviteChannelRequestDto.java @@ -0,0 +1,8 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import java.util.UUID; + +public record InviteChannelRequestDto( + UUID channelId +) { +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java index cee99c944..06e4b0f50 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/user/UserResponseDto.java @@ -1,9 +1,12 @@ package com.sprint.mission.discodeit.application.dto.user; +import com.sprint.mission.discodeit.domain.userstatus.enums.OnlineStatus; + public record UserResponseDto( String nickname, String username, - String email + String email, + OnlineStatus onlineStatus ) { } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index 7e4c20340..b07e6e983 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -5,39 +5,74 @@ import com.sprint.mission.discodeit.application.dto.channel.ChannelResponseDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.InviteChannelRequestDto; import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; +import com.sprint.mission.discodeit.domain.channel.enums.ChannelVisibility; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; +import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import com.sprint.mission.discodeit.repository.readstatus.interfaces.ReadStatusRepository; import java.util.UUID; public class JCFChannelService implements ChannelService { private final ChannelRepository channelRepository; private final UserService userService; + private final ReadStatusRepository readStatusRepository; public JCFChannelService( ChannelRepository channelRepository, - UserService userService + UserService userService, + ReadStatusRepository readStatusService ) { this.channelRepository = channelRepository; this.userService = userService; + this.readStatusRepository = readStatusService; } @Override - public ChannelResponseDto create(UUID userId, CreateChannelRequestDto requestDto) { + public ChannelResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - // TODO 예외 발생할 수 있음 -> 이넘타입에 존재하지 않을 경우 - ChannelType channelType = ChannelType.valueOf(requestDto.channelType()); - Channel createChannel = new Channel(requestDto.name(), channelType, foundUser); + // TODO 예외 발생할 수 있음 -> Request에 이넘타입에 존재하지 않을 경우 + ChannelType channelType = requestDto.channelType(); + Channel createChannel = new Channel(requestDto.name(), channelType, foundUser, ChannelVisibility.PUBLIC); Channel savedChannel = channelRepository.save(createChannel); return ChannelResponseDto.from(savedChannel); } + @Override + public ChannelResponseDto createPrivateChannel(UUID userId) { + User foundUser = userService.findOneByIdOrThrow(userId); + Channel createChannel = Channel.ofPrivateChannel(foundUser, ChannelType.TEXT); + Channel savedChannel = channelRepository.save(createChannel); + ReadStatus readStatus = new ReadStatus(foundUser, savedChannel); + readStatusRepository.save(readStatus); + return ChannelResponseDto.from(savedChannel); + } + + @Override + public void joinPublicChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(invitedUserId); + Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + foundChannel.join(foundUser); + channelRepository.save(foundChannel); + } + + @Override + public void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(invitedUserId); + Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + foundChannel.join(foundUser); + ReadStatus readStatus = new ReadStatus(foundUser, foundChannel); + readStatusRepository.save(readStatus); + channelRepository.save(foundChannel); + } + @Override public Channel findOneByIdOrThrow(UUID id) { return channelRepository.findOneById(id) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java index 3d94a2a1a..14dd7d728 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java @@ -5,12 +5,19 @@ import com.sprint.mission.discodeit.application.dto.channel.ChannelResponseDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.InviteChannelRequestDto; import com.sprint.mission.discodeit.domain.channel.Channel; import java.util.UUID; public interface ChannelService { - ChannelResponseDto create(UUID userId, CreateChannelRequestDto requestDto); + ChannelResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto); + + ChannelResponseDto createPrivateChannel(UUID userId); + + void joinPublicChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); + + void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); Channel findOneByIdOrThrow(UUID uuid); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java index ca3654f36..836c2bdb0 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java @@ -5,6 +5,7 @@ import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; import com.sprint.mission.discodeit.domain.user.User; +import java.util.List; import java.util.UUID; public interface UserService { @@ -15,6 +16,9 @@ public interface UserService { User findOneByIdOrThrow(UUID userId); + List findAll(); + void changePassword(UUID userId, ChangePasswordRequestDto requestDto); + void quitUser(UUID userId); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java index 73ddc6646..c809e7a0e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java @@ -7,6 +7,7 @@ import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; +import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.Nickname; @@ -18,8 +19,10 @@ import com.sprint.mission.discodeit.domain.user.exception.InvalidLoginException; import com.sprint.mission.discodeit.domain.user.exception.UserNotFoundException; import com.sprint.mission.discodeit.domain.user.validation.PasswordValidator; +import com.sprint.mission.discodeit.domain.userstatus.UserStatus; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.util.List; import java.util.UUID; import org.springframework.stereotype.Service; @@ -29,15 +32,18 @@ public class JCFUserService implements UserService { private final UserRepository userRepository; private final UserConverter userConverter; private final PasswordEncoder passwordEncoder; + private final UserStatusService userStatusService; public JCFUserService( UserRepository userRepository, UserConverter userConverter, - PasswordEncoder passwordEncoder + PasswordEncoder passwordEncoder, + UserStatusService userStatusService ) { this.userRepository = userRepository; this.userConverter = userConverter; this.passwordEncoder = passwordEncoder; + this.userStatusService = userStatusService; } @Override @@ -46,7 +52,8 @@ public UserResponseDto join(joinUserRequestDto requestDto) { throwUsernameAlreadyUsed(requestDto.username()); PasswordValidator.validateOrThrow(requestDto.password()); User savedUser = userRepository.save(toUserWithPasswordEncode(requestDto)); - return userConverter.toDto(savedUser); + UserStatus savedUserStatus = userStatusService.createAtFirstJoin(savedUser); + return userConverter.toDto(savedUser, savedUserStatus); } @Override @@ -64,6 +71,17 @@ public User findOneByIdOrThrow(UUID uuid) { .orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND)); } + @Override + public List findAll() { + List users = userRepository.findAll(); + return users.stream() + .map(user -> { + UserStatus userStatus = userStatusService.findOneByUser(user); + return userConverter.toDto(user, userStatus); + }) + .toList(); + } + @Override public void changePassword(UUID userId, ChangePasswordRequestDto requestDto) { PasswordValidator.validateOrThrow(requestDto.password()); @@ -72,6 +90,13 @@ public void changePassword(UUID userId, ChangePasswordRequestDto requestDto) { userRepository.save(foundUser); } + @Override + public void quitUser(UUID userId) { + User foundUser = findOneByIdOrThrow(userId); + userStatusService.delete(foundUser); + userRepository.deleteByUser(foundUser); + } + private void throwEmailAlreadyUsed(String email) { if (userRepository.isExistByEmail(new Email(email))) { throw new AlreadyUserExistsException(ErrorCode.DUPLICATE_EMAIL, email); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java index 79c3affb8..cd9e6aa38 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/converter/UserConverter.java @@ -1,31 +1,15 @@ package com.sprint.mission.discodeit.application.service.user.converter; -import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; -import com.sprint.mission.discodeit.domain.user.BirthDate; -import com.sprint.mission.discodeit.domain.user.Email; -import com.sprint.mission.discodeit.domain.user.Nickname; -import com.sprint.mission.discodeit.domain.user.Password; import com.sprint.mission.discodeit.domain.user.User; -import com.sprint.mission.discodeit.domain.user.Username; -import com.sprint.mission.discodeit.domain.user.enums.EmailSubscriptionStatus; +import com.sprint.mission.discodeit.domain.userstatus.UserStatus; import org.springframework.stereotype.Component; @Component public class UserConverter { - public User toUser(joinUserRequestDto requestDto) { - return User.builder() - .username(new Username(requestDto.username())) - .nickname(new Nickname(requestDto.nickname())) - .email(new Email(requestDto.email())) - .password(new Password(requestDto.password())) - .birthDate(new BirthDate(requestDto.birthDate())) - .emailSubscriptionStatus(EmailSubscriptionStatus.UNSUBSCRIBED) - .build(); - } - - public UserResponseDto toDto(User user) { - return new UserResponseDto(user.getNicknameValue(),user.getUsernameValue(),user.getEmailValue()); + public UserResponseDto toDto(User user, UserStatus userStatus) { + return new UserResponseDto(user.getNicknameValue(), user.getUsernameValue(), user.getEmailValue(), + userStatus.getOnlineStatus()); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java index c53f109e7..9ae4e28a9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java @@ -6,7 +6,9 @@ import com.sprint.mission.discodeit.domain.userstatus.enums.OnlineStatus; import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; import java.util.UUID; +import org.springframework.stereotype.Service; +@Service public class UserStatusService { private final UserStatusRepository userStatusRepository; @@ -20,6 +22,10 @@ public UserStatusService( this.userService = userService; } + public UserStatus createAtFirstJoin(User user) { + return userStatusRepository.save(new UserStatus(user)); + } + public UserStatus findOneByUser(User user) { return userStatusRepository.findByUser(user).orElse(new UserStatus(user)); } @@ -31,9 +37,13 @@ public void updateLastAccessTime(UUID userId) { userStatusRepository.save(foundUserStatus); } - public OnlineStatus getUserStatus(UUID userId) { + public OnlineStatus getUserOlineStatus(UUID userId) { User foundUser = userService.findOneByIdOrThrow(userId); UserStatus foundUserStatus = findOneByUser(foundUser); return foundUserStatus.getOnlineStatus(); } + + public void delete(User targetUser) { + userStatusRepository.deleteByUser(targetUser); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index 90595fa62..600fe922b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.domain.channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; +import com.sprint.mission.discodeit.domain.channel.enums.ChannelVisibility; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNameInvalidException; import com.sprint.mission.discodeit.domain.channel.exception.ChannelSubjectOverLengthException; import com.sprint.mission.discodeit.domain.user.User; @@ -25,20 +26,34 @@ public class Channel implements Serializable { private final Instant createdAt; private Instant updatedAt; private final User manager; + private final ChannelVisibility visibility; + private final ParticipatedUser participatedUser; + public Channel( String name, ChannelType type, - User manager + User manager, + ChannelVisibility visibility ) { validate(name); this.id = UUID.randomUUID(); this.name = name; this.subject = ""; this.type = type; + this.visibility = visibility; createdAt = Instant.now(); updatedAt = Instant.now(); this.manager = manager; + participatedUser = new ParticipatedUser(); + } + + public static Channel ofPrivateChannel(User manager, ChannelType type) { + return new Channel("", type, manager, ChannelVisibility.PRIVATE); + } + + public void join(User user) { + participatedUser.addUser(user); } public void updateSubject(String subject) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java new file mode 100644 index 000000000..70a62ff11 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java @@ -0,0 +1,15 @@ +package com.sprint.mission.discodeit.domain.channel; + +import com.sprint.mission.discodeit.domain.user.User; +import java.util.ArrayList; +import java.util.List; + +public class ParticipatedUser { + + private final List users = new ArrayList<>(); + + public void addUser(User user) { + users.add(user); + } + +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelVisibility.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelVisibility.java new file mode 100644 index 000000000..ba1034095 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/enums/ChannelVisibility.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.domain.channel.enums; + +public enum ChannelVisibility { + PUBLIC, + PRIVATE, +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorResponse.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorResponse.java new file mode 100644 index 000000000..4b52875a9 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorResponse.java @@ -0,0 +1,44 @@ +package com.sprint.mission.discodeit.global.error; + +import com.sprint.mission.discodeit.global.error.exception.BusinessException; +import java.util.ArrayList; +import java.util.List; + +public class ErrorResponse { + + private String description; + private int status; + private List fieldErrors; + + protected ErrorResponse() { + } + + public ErrorResponse(String description, int status) { + this.description = description; + this.status = status; + fieldErrors = new ArrayList<>(); + } + + public ErrorResponse(BusinessException businessException) { + this.description = businessException.getErrorCode().getDescription(); + this.status = businessException.getErrorCode().getStatus(); + } + + public String getDescription() { + return description; + } + + public int getStatus() { + return status; + } + + public List getFieldErrors() { + return fieldErrors; + } + + public static class FieldError { + private String field; + private String value; + private String reason; + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java new file mode 100644 index 000000000..ad7c110aa --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java @@ -0,0 +1,39 @@ +package com.sprint.mission.discodeit.global.error; + +import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalException { + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception) { + ErrorResponse errorResponse = new ErrorResponse(); + return ResponseEntity.badRequest().body(errorResponse); + } + + @ExceptionHandler(InvalidException.class) + protected ResponseEntity handleInvalidException(InvalidException exception) { + ErrorResponse errorResponse = new ErrorResponse(exception.getErrorCode().getDescription(), + exception.getErrorCode().getStatus()); + return ResponseEntity.badRequest().body(errorResponse); + } + + @ExceptionHandler(EntityNotFoundException.class) + protected ResponseEntity handleEntityNotFoundException(EntityNotFoundException exception) { + ErrorResponse errorResponse = new ErrorResponse(exception.getErrorCode().getDescription(), + exception.getErrorCode().getStatus()); + return ResponseEntity.status(404).body(errorResponse); + } + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(Exception exception) { + ErrorResponse errorResponse = new ErrorResponse(); + return ResponseEntity.internalServerError().body(errorResponse); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java index cfe26b86e..917cf9764 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java @@ -17,4 +17,8 @@ public BusinessException(ErrorCode errorCode, String message) { super(errorCode.getDescription().concat(" 입력값 = ").concat(message)); this.errorCode = errorCode; } + + public ErrorCode getErrorCode() { + return errorCode; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java index 8ef29fb52..afb705e9c 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/FileUserRepository.java @@ -4,6 +4,7 @@ import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -24,6 +25,11 @@ public Optional findOneByEmail(Email email) { return Optional.empty(); } + @Override + public List findAll() { + return List.of(); + } + @Override public boolean isExistByEmail(Email email) { return false; @@ -33,4 +39,9 @@ public boolean isExistByEmail(Email email) { public boolean isExistByUsername(Username username) { return false; } + + @Override + public void deleteByUser(User user) { + + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java index 07d426bef..5c8068029 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -5,6 +5,7 @@ import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -35,6 +36,11 @@ public Optional findOneByEmail(Email email) { return Optional.ofNullable(emailUsers.get(email)); } + @Override + public List findAll() { + return uuidUsers.values().stream().toList(); + } + @Override public boolean isExistByEmail(Email email) { return emailUsers.containsKey(email); @@ -44,4 +50,11 @@ public boolean isExistByEmail(Email email) { public boolean isExistByUsername(Username username) { return usernameUsers.containsKey(username); } + + @Override + public void deleteByUser(User user) { + uuidUsers.remove(user.getId()); + emailUsers.remove(user.getEmail()); + usernameUsers.remove(user.getUsername()); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java index 3b32b09d1..d7cc2dcc4 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/interfaces/UserRepository.java @@ -3,6 +3,7 @@ import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.domain.user.Username; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -14,7 +15,11 @@ public interface UserRepository { Optional findOneByEmail(Email email); + List findAll(); + boolean isExistByEmail(Email email); boolean isExistByUsername(Username username); + + void deleteByUser(User user); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java index 4f9e72b9e..3baf86c5a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java @@ -6,7 +6,9 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import org.springframework.stereotype.Repository; +@Repository public class UserStatusInMemoryRepository implements UserStatusRepository { private final Map userStatuses = new HashMap<>(); @@ -22,4 +24,9 @@ public UserStatus save(UserStatus userStatus) { public Optional findByUser(User user) { return Optional.ofNullable(userStatuses.get(user)); } + + @Override + public void deleteByUser(User user) { + userStatuses.remove(user); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java index d66ef1b09..faf046735 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java @@ -9,4 +9,6 @@ public interface UserStatusRepository { UserStatus save(UserStatus userStatus); Optional findByUser(User user); + + void deleteByUser(User user); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java index c0e18306f..5f385f02c 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/repository/FakeUserRepository.java @@ -5,6 +5,7 @@ import com.sprint.mission.discodeit.domain.user.Username; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -33,6 +34,11 @@ public Optional findOneByEmail(Email email) { return Optional.ofNullable(emailUsers.get(email)); } + @Override + public List findAll() { + return uuidUsers.values().stream().toList(); + } + @Override public boolean isExistByEmail(Email email) { return emailUsers.containsKey(email); @@ -42,4 +48,11 @@ public boolean isExistByEmail(Email email) { public boolean isExistByUsername(Username username) { return usernameUsers.containsKey(username); } + + @Override + public void deleteByUser(User user) { + uuidUsers.remove(user.getId()); + emailUsers.remove(user.getEmail()); + usernameUsers.remove(user.getUsername()); + } } From f27e819ae17232110c114c29535a1759fd0404a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:55:04 +0900 Subject: [PATCH 27/38] =?UTF-8?q?feat:=20channel=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=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 --- .../mission/discodeit/JavaApplication.java | 11 +------ ...Dto.java => ChannelCreateResponseDto.java} | 6 ++-- .../dto/channel/FoundChannelResponseDto.java | 22 +++++++++++++ .../service/channel/JCFChannelService.java | 31 ++++++++++++++----- .../service/interfaces/ChannelService.java | 9 ++++-- .../service/interfaces/MessageService.java | 3 ++ .../service/message/JCFMessageService.java | 6 ++++ .../service/userstatus/UserStatusService.java | 17 +++------- .../discodeit/config/ChannelFactory.java | 10 ++++-- .../mission/discodeit/config/UserFactory.java | 14 +++++++-- .../discodeit/domain/channel/Channel.java | 12 +++++-- .../domain/channel/ParticipatedUser.java | 14 ++++++--- 12 files changed, 109 insertions(+), 46 deletions(-) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/{ChannelResponseDto.java => ChannelCreateResponseDto.java} (72%) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java index 31ed5e30b..a18709bd9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java @@ -1,17 +1,8 @@ package com.sprint.mission.discodeit; -import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; -import com.sprint.mission.discodeit.application.service.interfaces.MessageService; -import com.sprint.mission.discodeit.application.service.interfaces.UserService; -import com.sprint.mission.discodeit.config.ChannelFactory; -import com.sprint.mission.discodeit.config.MessageFactory; -import com.sprint.mission.discodeit.config.UserFactory; - public class JavaApplication { public static void main(String[] args) { - UserService userService = UserFactory.getUserService(); - ChannelService channelService = ChannelFactory.getChannelService(); - MessageService messageService = MessageFactory.getMessageService(); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelCreateResponseDto.java similarity index 72% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelCreateResponseDto.java index 2aafb3304..5737d0c49 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelResponseDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/ChannelCreateResponseDto.java @@ -3,15 +3,15 @@ import com.sprint.mission.discodeit.domain.channel.Channel; import java.util.UUID; -public record ChannelResponseDto( +public record ChannelCreateResponseDto( UUID channelId, String name, String subject, String channelType ) { - public static ChannelResponseDto from(Channel channel) { - return new ChannelResponseDto( + public static ChannelCreateResponseDto from(Channel channel) { + return new ChannelCreateResponseDto( channel.getId(), channel.getName(), channel.getSubject(), diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java new file mode 100644 index 000000000..ef8eea9ac --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java @@ -0,0 +1,22 @@ +package com.sprint.mission.discodeit.application.dto.channel; + +import com.sprint.mission.discodeit.domain.channel.Channel; +import java.time.LocalDateTime; +import java.util.Set; +import java.util.UUID; + +public record FoundChannelResponseDto( + UUID channelId, + String channelName, + LocalDateTime lastMessageTime, + Set participatedUserId +) { + public static FoundChannelResponseDto ofPublicChannel(Channel channel, LocalDateTime lastMessageTime) { + return new FoundChannelResponseDto(channel.getId(), channel.getName(), lastMessageTime, Set.of()); + } + + public static FoundChannelResponseDto ofPrivateChannel(Channel channel, LocalDateTime lastMessageTime) { + return new FoundChannelResponseDto(channel.getId(), channel.getName(), lastMessageTime, + channel.getParticipantUserId()); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index b07e6e983..f5ba9eaea 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -2,11 +2,13 @@ import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelNameRequestDto; import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelSubjectRequestDto; -import com.sprint.mission.discodeit.application.dto.channel.ChannelResponseDto; +import com.sprint.mission.discodeit.application.dto.channel.ChannelCreateResponseDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.InviteChannelRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.FoundChannelResponseDto; import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; +import com.sprint.mission.discodeit.application.service.interfaces.MessageService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; @@ -17,6 +19,7 @@ import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import com.sprint.mission.discodeit.repository.readstatus.interfaces.ReadStatusRepository; +import java.time.LocalDateTime; import java.util.UUID; public class JCFChannelService implements ChannelService { @@ -24,35 +27,37 @@ public class JCFChannelService implements ChannelService { private final ChannelRepository channelRepository; private final UserService userService; private final ReadStatusRepository readStatusRepository; + private final MessageService messageService; public JCFChannelService( ChannelRepository channelRepository, UserService userService, - ReadStatusRepository readStatusService + ReadStatusRepository readStatusService, + MessageService messageService ) { this.channelRepository = channelRepository; this.userService = userService; this.readStatusRepository = readStatusService; + this.messageService = messageService; } @Override - public ChannelResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto) { + public ChannelCreateResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - // TODO 예외 발생할 수 있음 -> Request에 이넘타입에 존재하지 않을 경우 ChannelType channelType = requestDto.channelType(); Channel createChannel = new Channel(requestDto.name(), channelType, foundUser, ChannelVisibility.PUBLIC); Channel savedChannel = channelRepository.save(createChannel); - return ChannelResponseDto.from(savedChannel); + return ChannelCreateResponseDto.from(savedChannel); } @Override - public ChannelResponseDto createPrivateChannel(UUID userId) { + public ChannelCreateResponseDto createPrivateChannel(UUID userId) { User foundUser = userService.findOneByIdOrThrow(userId); Channel createChannel = Channel.ofPrivateChannel(foundUser, ChannelType.TEXT); Channel savedChannel = channelRepository.save(createChannel); ReadStatus readStatus = new ReadStatus(foundUser, savedChannel); readStatusRepository.save(readStatus); - return ChannelResponseDto.from(savedChannel); + return ChannelCreateResponseDto.from(savedChannel); } @Override @@ -73,6 +78,18 @@ public void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto reque channelRepository.save(foundChannel); } + @Override + public FoundChannelResponseDto findOneByChannelId(UUID channelId) { + Channel foundChannel = findOneByIdOrThrow(channelId); + LocalDateTime lastMessageTime = messageService.getLastMessageTime(foundChannel.getId()); + if (foundChannel.isPublic()) { + return FoundChannelResponseDto.ofPublicChannel(foundChannel, lastMessageTime); + } + else { + return FoundChannelResponseDto.ofPrivateChannel(foundChannel, lastMessageTime); + } + } + @Override public Channel findOneByIdOrThrow(UUID id) { return channelRepository.findOneById(id) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java index 14dd7d728..a67d53b28 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java @@ -2,23 +2,26 @@ import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelNameRequestDto; import com.sprint.mission.discodeit.application.dto.channel.ChangeChannelSubjectRequestDto; -import com.sprint.mission.discodeit.application.dto.channel.ChannelResponseDto; +import com.sprint.mission.discodeit.application.dto.channel.ChannelCreateResponseDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; +import com.sprint.mission.discodeit.application.dto.channel.FoundChannelResponseDto; import com.sprint.mission.discodeit.application.dto.channel.InviteChannelRequestDto; import com.sprint.mission.discodeit.domain.channel.Channel; import java.util.UUID; public interface ChannelService { - ChannelResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto); + ChannelCreateResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto); - ChannelResponseDto createPrivateChannel(UUID userId); + ChannelCreateResponseDto createPrivateChannel(UUID userId); void joinPublicChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); + FoundChannelResponseDto findOneByChannelId(UUID channelId); + Channel findOneByIdOrThrow(UUID uuid); void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java index 49574fa84..ce4e9bf46 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java @@ -5,6 +5,7 @@ import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; import com.sprint.mission.discodeit.application.dto.message.UpdateMessageContentRequestDto; import com.sprint.mission.discodeit.domain.message.Message; +import java.time.LocalDateTime; import java.util.UUID; public interface MessageService { @@ -16,4 +17,6 @@ public interface MessageService { void deleteMessage(UUID userId, DeleteMessageRequestDto requestDto); Message findOneByIdOrThrow(UUID uuid); + + LocalDateTime getLastMessageTime(UUID channelId); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index 0bd4514b9..69431df74 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -13,6 +13,7 @@ import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.time.LocalDateTime; import java.util.UUID; public class JCFMessageService implements MessageService { @@ -62,6 +63,11 @@ public Message findOneByIdOrThrow(UUID messageId) { .orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); } + @Override + public LocalDateTime getLastMessageTime(UUID channelId) { + return null; + } + private void throwIsNotSender(User foundUser, Message foundMessage) { if (!foundMessage.isSender(foundUser)) { throw new IllegalArgumentException(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java index 9ae4e28a9..fd027d44f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java @@ -1,25 +1,20 @@ package com.sprint.mission.discodeit.application.service.userstatus; -import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.domain.userstatus.UserStatus; import com.sprint.mission.discodeit.domain.userstatus.enums.OnlineStatus; import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; -import java.util.UUID; import org.springframework.stereotype.Service; @Service public class UserStatusService { private final UserStatusRepository userStatusRepository; - private final UserService userService; public UserStatusService( - UserStatusRepository userStatusRepository, - UserService userService + UserStatusRepository userStatusRepository ) { this.userStatusRepository = userStatusRepository; - this.userService = userService; } public UserStatus createAtFirstJoin(User user) { @@ -30,16 +25,14 @@ public UserStatus findOneByUser(User user) { return userStatusRepository.findByUser(user).orElse(new UserStatus(user)); } - public void updateLastAccessTime(UUID userId) { - User foundUser = userService.findOneByIdOrThrow(userId); - UserStatus foundUserStatus = findOneByUser(foundUser); + public void updateLastAccessTime(User user) { + UserStatus foundUserStatus = findOneByUser(user); foundUserStatus.updateLastAccessedAt(); userStatusRepository.save(foundUserStatus); } - public OnlineStatus getUserOlineStatus(UUID userId) { - User foundUser = userService.findOneByIdOrThrow(userId); - UserStatus foundUserStatus = findOneByUser(foundUser); + public OnlineStatus getUserOlineStatus(User user) { + UserStatus foundUserStatus = findOneByUser(user); return foundUserStatus.getOnlineStatus(); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java index b7df9f9ab..b187db029 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java @@ -4,7 +4,7 @@ import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.repository.channel.InMemoryChannelRepository; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; -import java.io.FileInputStream; +import com.sprint.mission.discodeit.repository.readstatus.ReadStatusInMemoryRepository; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -12,7 +12,13 @@ public class ChannelFactory { private static final ChannelRepository CHANNEL_REPOSITORY = createChannelRepository(); - private static final ChannelService CHANNEL_SERVICE = new JCFChannelService(CHANNEL_REPOSITORY, UserFactory.getUserService()); + private static final ChannelService CHANNEL_SERVICE = + new JCFChannelService( + CHANNEL_REPOSITORY, + UserFactory.getUserService(), + new ReadStatusInMemoryRepository(), + MessageFactory.getMessageService() + ); public static ChannelService getChannelService() { return CHANNEL_SERVICE; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java index 88458468e..cbbe58db9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java @@ -4,9 +4,11 @@ import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.application.service.user.JCFUserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; +import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; import com.sprint.mission.discodeit.repository.user.InMemoryUserRepository; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; -import java.io.FileInputStream; +import com.sprint.mission.discodeit.repository.userstatus.UserStatusInMemoryRepository; +import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -14,7 +16,13 @@ public class UserFactory { private static final UserRepository USER_REPOSITORY = createUserRepository(); - private static final UserService USER_SERVICE = new JCFUserService(USER_REPOSITORY, new UserConverter(), new PasswordEncoder()); + private static final UserService USER_SERVICE = + new JCFUserService( + USER_REPOSITORY, + new UserConverter(), + new PasswordEncoder(), + new UserStatusService(new UserStatusInMemoryRepository()) + ); public static UserService getUserService() { return USER_SERVICE; @@ -29,7 +37,7 @@ public static UserRepository createUserRepository() { } String repositoryType = properties.getProperty("repository.type", "memory"); - return switch(repositoryType) { + return switch (repositoryType) { default -> new InMemoryUserRepository(); }; } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index 600fe922b..c4fdd4630 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -9,8 +9,8 @@ import java.io.Serial; import java.io.Serializable; import java.time.Instant; -import java.time.LocalDateTime; import java.util.Objects; +import java.util.Set; import java.util.UUID; public class Channel implements Serializable { @@ -29,7 +29,6 @@ public class Channel implements Serializable { private final ChannelVisibility visibility; private final ParticipatedUser participatedUser; - public Channel( String name, ChannelType type, @@ -78,6 +77,14 @@ public boolean isManager(User user) { return this.manager.equals(user); } + public boolean isPublic() { + return this.visibility == ChannelVisibility.PUBLIC; + } + + public Set getParticipantUserId() { + return this.participatedUser.getParticipatedUserId(); + } + public UUID getId() { return id; } @@ -94,6 +101,7 @@ public String getType() { return type.toString(); } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java index 70a62ff11..1c7a7bd99 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java @@ -1,15 +1,21 @@ package com.sprint.mission.discodeit.domain.channel; import com.sprint.mission.discodeit.domain.user.User; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; public class ParticipatedUser { - private final List users = new ArrayList<>(); + private final Map users = new HashMap<>(); public void addUser(User user) { - users.add(user); + users.put(user.getId(), user); } + public Set getParticipatedUserId() { + return Collections.unmodifiableSet(users.keySet()); + } } From f809a2470e03b3346e9d5c6f9b6970651a9bdca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:46:52 +0900 Subject: [PATCH 28/38] =?UTF-8?q?feat:=20channel=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B3=A0=EB=8F=84=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/channel/JCFChannelService.java | 36 ++++++++++++------- .../service/interfaces/MessageService.java | 2 -- .../service/message/JCFMessageService.java | 5 --- .../discodeit/config/ChannelFactory.java | 3 +- .../channel/FileChannelRepository.java | 6 ++++ .../channel/InMemoryChannelRepository.java | 6 ++++ .../channel/interfaces/ChannelRepository.java | 3 ++ .../message/FileMessageRepository.java | 12 +++++++ .../message/InMemoryMessageRepository.java | 12 +++++++ .../message/interfaces/MessageRepository.java | 6 ++++ .../ReadStatusInMemoryRepository.java | 21 ++++++++--- .../interfaces/ReadStatusRepository.java | 2 ++ .../mission/discodeit/fake/FakeFactory.java | 9 ++++- 13 files changed, 97 insertions(+), 26 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index f5ba9eaea..8b02bd7c9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -5,10 +5,9 @@ import com.sprint.mission.discodeit.application.dto.channel.ChannelCreateResponseDto; import com.sprint.mission.discodeit.application.dto.channel.CreateChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.DeleteChannelRequestDto; -import com.sprint.mission.discodeit.application.dto.channel.InviteChannelRequestDto; import com.sprint.mission.discodeit.application.dto.channel.FoundChannelResponseDto; +import com.sprint.mission.discodeit.application.dto.channel.InviteChannelRequestDto; import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; -import com.sprint.mission.discodeit.application.service.interfaces.MessageService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; @@ -18,8 +17,10 @@ import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; import com.sprint.mission.discodeit.repository.readstatus.interfaces.ReadStatusRepository; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; public class JCFChannelService implements ChannelService { @@ -27,18 +28,18 @@ public class JCFChannelService implements ChannelService { private final ChannelRepository channelRepository; private final UserService userService; private final ReadStatusRepository readStatusRepository; - private final MessageService messageService; + private final MessageRepository messageRepository; public JCFChannelService( ChannelRepository channelRepository, UserService userService, ReadStatusRepository readStatusService, - MessageService messageService + MessageRepository messageRepository ) { this.channelRepository = channelRepository; this.userService = userService; this.readStatusRepository = readStatusService; - this.messageService = messageService; + this.messageRepository = messageRepository; } @Override @@ -81,13 +82,13 @@ public void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto reque @Override public FoundChannelResponseDto findOneByChannelId(UUID channelId) { Channel foundChannel = findOneByIdOrThrow(channelId); - LocalDateTime lastMessageTime = messageService.getLastMessageTime(foundChannel.getId()); - if (foundChannel.isPublic()) { - return FoundChannelResponseDto.ofPublicChannel(foundChannel, lastMessageTime); - } - else { - return FoundChannelResponseDto.ofPrivateChannel(foundChannel, lastMessageTime); - } + return toFoundChannelResponseDto(foundChannel); + } + + public List findAllByChannelId(UUID userId) { + List channels = channelRepository.findAllByUserId(userId); + // 채널 한 개당 메세지를 조회해오는 N + 1 + return channels.stream().map(this::toFoundChannelResponseDto).toList(); } @Override @@ -119,6 +120,8 @@ public void deleteChannel(UUID userId, DeleteChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); + readStatusRepository.deleteByChannel(foundChannel); + messageRepository.deleteByChannel(foundChannel); channelRepository.deleteById(foundChannel.getId()); } @@ -127,4 +130,13 @@ private void throwIsNotManager(User foundUser, Channel foundChannel) { throw new IllegalArgumentException(); } } + + private FoundChannelResponseDto toFoundChannelResponseDto(Channel foundChannel) { + LocalDateTime lastMessageTime = messageRepository.getLastMessageTimeByChannelId(foundChannel.getId()); + if (foundChannel.isPublic()) { + return FoundChannelResponseDto.ofPublicChannel(foundChannel, lastMessageTime); + } else { + return FoundChannelResponseDto.ofPrivateChannel(foundChannel, lastMessageTime); + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java index ce4e9bf46..d1a8b44ec 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java @@ -5,7 +5,6 @@ import com.sprint.mission.discodeit.application.dto.message.MessageResponseDto; import com.sprint.mission.discodeit.application.dto.message.UpdateMessageContentRequestDto; import com.sprint.mission.discodeit.domain.message.Message; -import java.time.LocalDateTime; import java.util.UUID; public interface MessageService { @@ -18,5 +17,4 @@ public interface MessageService { Message findOneByIdOrThrow(UUID uuid); - LocalDateTime getLastMessageTime(UUID channelId); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index 69431df74..88d2b0c7d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -63,11 +63,6 @@ public Message findOneByIdOrThrow(UUID messageId) { .orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); } - @Override - public LocalDateTime getLastMessageTime(UUID channelId) { - return null; - } - private void throwIsNotSender(User foundUser, Message foundMessage) { if (!foundMessage.isSender(foundUser)) { throw new IllegalArgumentException(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java index b187db029..8f7928df0 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java @@ -4,6 +4,7 @@ import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.repository.channel.InMemoryChannelRepository; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import com.sprint.mission.discodeit.repository.message.InMemoryMessageRepository; import com.sprint.mission.discodeit.repository.readstatus.ReadStatusInMemoryRepository; import java.io.IOException; import java.io.InputStream; @@ -17,7 +18,7 @@ public class ChannelFactory { CHANNEL_REPOSITORY, UserFactory.getUserService(), new ReadStatusInMemoryRepository(), - MessageFactory.getMessageService() + new InMemoryMessageRepository() ); public static ChannelService getChannelService() { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java index ee79d46f7..c4cc0cd8d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -16,6 +17,11 @@ public Optional findOneById(UUID uuid) { return Optional.empty(); } + @Override + public List findAllByUserId(UUID userId) { + return List.of(); + } + @Override public void deleteById(UUID uuid) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java index b133fb177..3bbc44033 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java @@ -3,6 +3,7 @@ import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -22,6 +23,11 @@ public Optional findOneById(UUID uuid) { return Optional.ofNullable(uuidChannels.get(uuid)); } + @Override + public List findAllByUserId(UUID userId) { + return List.of(); + } + @Override public void deleteById(UUID uuid) { uuidChannels.remove(uuid); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java index 767a7f1ee..c27171617 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.repository.channel.interfaces; import com.sprint.mission.discodeit.domain.channel.Channel; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -10,5 +11,7 @@ public interface ChannelRepository { Optional findOneById(UUID uuid); + List findAllByUserId(UUID userId); + void deleteById(UUID uuid); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java index 83a328d74..c954da63b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java @@ -1,7 +1,9 @@ package com.sprint.mission.discodeit.repository.message; +import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; @@ -20,4 +22,14 @@ public Optional findById(UUID uuid) { public void deleteById(UUID uuid) { } + + @Override + public void deleteByChannel(Channel channel) { + + } + + @Override + public LocalDateTime getLastMessageTimeByChannelId(UUID channelId) { + return null; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java index 20324e84a..1bf305b08 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java @@ -1,7 +1,9 @@ package com.sprint.mission.discodeit.repository.message; +import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; +import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; @@ -20,4 +22,14 @@ public Optional findById(UUID uuid) { public void deleteById(UUID uuid) { } + + @Override + public void deleteByChannel(Channel channel) { + + } + + @Override + public LocalDateTime getLastMessageTimeByChannelId(UUID channelId) { + return null; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java index 252d39325..1560b2db9 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/interfaces/MessageRepository.java @@ -1,6 +1,8 @@ package com.sprint.mission.discodeit.repository.message.interfaces; +import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; +import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; @@ -11,4 +13,8 @@ public interface MessageRepository { Optional findById(UUID uuid); void deleteById(UUID uuid); + + void deleteByChannel(Channel channel); + + LocalDateTime getLastMessageTimeByChannelId(UUID channelId); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java index 41a252029..05622e660 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java @@ -16,24 +16,35 @@ public class ReadStatusInMemoryRepository implements ReadStatusRepository { @Override public ReadStatus save(ReadStatus readStatus) { - ReadStatusKey readStatusKey = new ReadStatusKey(readStatus.getUser(), readStatus.getChannel()); + ReadStatusKey readStatusKey = ReadStatusKey.of(readStatus.getUser(), readStatus.getChannel()); readStatusMap.put(readStatusKey, readStatus); return readStatus; } @Override public Optional findOneByUserIdAndChannelId(User user, Channel channel) { - ReadStatusKey readStatusKey = new ReadStatusKey(user, channel); + ReadStatusKey readStatusKey = ReadStatusKey.of(user, channel); return Optional.ofNullable(readStatusMap.get(readStatusKey)); } + @Override + public void deleteByChannel(Channel channel) { + channel.getParticipantUserId().stream() + .map(userId -> new ReadStatusKey(userId, channel.getId())) + .forEach(readStatusMap::remove); + } + private static class ReadStatusKey { private final UUID userId; private final UUID channelId; - public ReadStatusKey(User user, Channel channel) { - this.userId = user.getId(); - this.channelId = channel.getId(); + public ReadStatusKey(UUID userId, UUID channelId) { + this.userId = userId; + this.channelId = channelId; + } + + public static ReadStatusKey of(User user, Channel channel) { + return new ReadStatusKey(user.getId(), channel.getId()); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java index ff07a73ae..909fcb098 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/interfaces/ReadStatusRepository.java @@ -10,4 +10,6 @@ public interface ReadStatusRepository { ReadStatus save(ReadStatus readStatus); Optional findOneByUserIdAndChannelId(User user, Channel channel); + + void deleteByChannel(Channel channel); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java index d3da490c5..18e324880 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java @@ -3,8 +3,11 @@ import com.sprint.mission.discodeit.application.auth.PasswordEncoder; import com.sprint.mission.discodeit.application.service.user.JCFUserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; +import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; import com.sprint.mission.discodeit.fake.repository.FakeUserRepository; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; +import com.sprint.mission.discodeit.repository.userstatus.UserStatusInMemoryRepository; +import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; public class FakeFactory { private FakeFactory() { @@ -15,6 +18,10 @@ public static UserRepository getUserRepository() { } public static JCFUserService getUserService() { - return new JCFUserService(getUserRepository(), new UserConverter(), new PasswordEncoder()); + return new JCFUserService( + getUserRepository(), + new UserConverter(), + new PasswordEncoder(), + new UserStatusService(new UserStatusInMemoryRepository())); } } From 6a468397bb77f107591038899510cfd7d30ef697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:33:34 +0900 Subject: [PATCH 29/38] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EA=B8=B0=EB=8A=A5=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 --- .../service/channel/JCFChannelService.java | 8 ++++++++ .../service/userstatus/UserStatusService.java | 3 +++ .../exception/AlreadyJoinUserException.java | 11 +++++++++++ .../discodeit/global/error/ErrorCode.java | 2 +- .../channel/FileChannelRepository.java | 6 ++++++ .../channel/InMemoryChannelRepository.java | 16 ++++++++++++---- .../channel/interfaces/ChannelRepository.java | 3 +++ .../readstatus/ReadStatusInMemoryRepository.java | 8 ++++---- .../repository/user/InMemoryUserRepository.java | 2 +- .../userstatus/UserStatusInMemoryRepository.java | 11 +++++++++++ .../interfaces/UserStatusRepository.java | 5 +++++ 11 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/AlreadyJoinUserException.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index 8b02bd7c9..5855789ed 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -12,6 +12,7 @@ import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.enums.ChannelVisibility; +import com.sprint.mission.discodeit.domain.channel.exception.AlreadyJoinUserException; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; import com.sprint.mission.discodeit.domain.user.User; @@ -73,6 +74,7 @@ public void joinPublicChannel(UUID invitedUserId, InviteChannelRequestDto reques public void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(invitedUserId); Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + throwIsAlreadyJoinUser(foundUser, foundChannel); foundChannel.join(foundUser); ReadStatus readStatus = new ReadStatus(foundUser, foundChannel); readStatusRepository.save(readStatus); @@ -139,4 +141,10 @@ private FoundChannelResponseDto toFoundChannelResponseDto(Channel foundChannel) return FoundChannelResponseDto.ofPrivateChannel(foundChannel, lastMessageTime); } } + + private void throwIsAlreadyJoinUser(User targetUser, Channel targetChannel) { + if (channelRepository.isExistUser(targetUser, targetChannel)) { + throw new AlreadyJoinUserException(ErrorCode.ALREADY_CHANNEL_JOIN_USER, targetUser.getNicknameValue()); + } + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java index fd027d44f..70e0ef0ab 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/userstatus/UserStatusService.java @@ -18,6 +18,9 @@ public UserStatusService( } public UserStatus createAtFirstJoin(User user) { + if (userStatusRepository.isExistUser(user)) { + throw new IllegalArgumentException("userStatus already exist"); + } return userStatusRepository.save(new UserStatus(user)); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/AlreadyJoinUserException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/AlreadyJoinUserException.java new file mode 100644 index 000000000..91835522d --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/AlreadyJoinUserException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.channel.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class AlreadyJoinUserException extends InvalidException { + + public AlreadyJoinUserException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index 9b88e3409..26003aa78 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -29,7 +29,7 @@ public enum ErrorCode { // Channel INVALID_CHANNEL_NAME_NOT_NULL(400, "채널 이름은 필수 입력값 입니다."), - + ALREADY_CHANNEL_JOIN_USER(400, "이미 참여한 채널입니다."), // Message INVALID_MESSAGE_CONTENT_NOT_NULL(400, "메시지 내용은 필수 입력값 입니다."), ; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java index c4cc0cd8d..8242c22ca 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.repository.channel; import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import java.util.List; import java.util.Optional; @@ -26,4 +27,9 @@ public List findAllByUserId(UUID userId) { public void deleteById(UUID uuid) { } + + @Override + public boolean isExistUser(User user, Channel channel) { + return false; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java index 3bbc44033..8aa30d5fe 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java @@ -1,26 +1,28 @@ package com.sprint.mission.discodeit.repository.channel; import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; public class InMemoryChannelRepository implements ChannelRepository { - private final Map uuidChannels = new HashMap<>(); + private final Map channelUUIDStore = new HashMap<>(); @Override public Channel save(Channel channel) { - Channel savedChannel = uuidChannels.put(channel.getId(), channel); + Channel savedChannel = channelUUIDStore.put(channel.getId(), channel); return channel; } @Override public Optional findOneById(UUID uuid) { - return Optional.ofNullable(uuidChannels.get(uuid)); + return Optional.ofNullable(channelUUIDStore.get(uuid)); } @Override @@ -30,6 +32,12 @@ public List findAllByUserId(UUID userId) { @Override public void deleteById(UUID uuid) { - uuidChannels.remove(uuid); + channelUUIDStore.remove(uuid); + } + + @Override + public boolean isExistUser(User user, Channel channel) { + Set participantUserId = channelUUIDStore.get(channel.getId()).getParticipantUserId(); + return participantUserId.contains(user.getId()); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java index c27171617..16df8f06e 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.repository.channel.interfaces; import com.sprint.mission.discodeit.domain.channel.Channel; +import com.sprint.mission.discodeit.domain.user.User; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -14,4 +15,6 @@ public interface ChannelRepository { List findAllByUserId(UUID userId); void deleteById(UUID uuid); + + boolean isExistUser(User user, Channel channel); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java index 05622e660..2eff1677b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java @@ -12,26 +12,26 @@ public class ReadStatusInMemoryRepository implements ReadStatusRepository { - private final Map readStatusMap = new HashMap<>(); + private final Map readStatusStore = new HashMap<>(); @Override public ReadStatus save(ReadStatus readStatus) { ReadStatusKey readStatusKey = ReadStatusKey.of(readStatus.getUser(), readStatus.getChannel()); - readStatusMap.put(readStatusKey, readStatus); + readStatusStore.put(readStatusKey, readStatus); return readStatus; } @Override public Optional findOneByUserIdAndChannelId(User user, Channel channel) { ReadStatusKey readStatusKey = ReadStatusKey.of(user, channel); - return Optional.ofNullable(readStatusMap.get(readStatusKey)); + return Optional.ofNullable(readStatusStore.get(readStatusKey)); } @Override public void deleteByChannel(Channel channel) { channel.getParticipantUserId().stream() .map(userId -> new ReadStatusKey(userId, channel.getId())) - .forEach(readStatusMap::remove); + .forEach(readStatusStore::remove); } private static class ReadStatusKey { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java index 5c8068029..54e981a4a 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java @@ -38,7 +38,7 @@ public Optional findOneByEmail(Email email) { @Override public List findAll() { - return uuidUsers.values().stream().toList(); + return List.copyOf(uuidUsers.values()); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java index 3baf86c5a..c2cbf8ba7 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/UserStatusInMemoryRepository.java @@ -4,6 +4,7 @@ import com.sprint.mission.discodeit.domain.userstatus.UserStatus; import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.springframework.stereotype.Repository; @@ -25,8 +26,18 @@ public Optional findByUser(User user) { return Optional.ofNullable(userStatuses.get(user)); } + @Override + public List findAll() { + return List.copyOf(userStatuses.values()); + } + @Override public void deleteByUser(User user) { userStatuses.remove(user); } + + @Override + public boolean isExistUser(User user) { + return userStatuses.containsKey(user); + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java index faf046735..168e69d1f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/userstatus/interfaces/UserStatusRepository.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.domain.userstatus.UserStatus; +import java.util.List; import java.util.Optional; public interface UserStatusRepository { @@ -10,5 +11,9 @@ public interface UserStatusRepository { Optional findByUser(User user); + List findAll(); + void deleteByUser(User user); + + boolean isExistUser(User user); } From 4cf1ba5b938c73de1b9749042423602e797168f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:36:29 +0900 Subject: [PATCH 30/38] =?UTF-8?q?refactor:=20application=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=99=95=EC=9E=A5=EC=9E=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discodeit/config/ChannelFactory.java | 41 ----------------- .../discodeit/config/MessageFactory.java | 34 -------------- .../mission/discodeit/config/UserFactory.java | 44 ------------------- .../src/main/resources/application.properties | 1 - .../src/main/resources/application.yaml | 0 5 files changed, 120 deletions(-) delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java delete mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java deleted file mode 100644 index 8f7928df0..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/ChannelFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.sprint.mission.discodeit.config; - -import com.sprint.mission.discodeit.application.service.channel.JCFChannelService; -import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; -import com.sprint.mission.discodeit.repository.channel.InMemoryChannelRepository; -import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; -import com.sprint.mission.discodeit.repository.message.InMemoryMessageRepository; -import com.sprint.mission.discodeit.repository.readstatus.ReadStatusInMemoryRepository; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -public class ChannelFactory { - - private static final ChannelRepository CHANNEL_REPOSITORY = createChannelRepository(); - private static final ChannelService CHANNEL_SERVICE = - new JCFChannelService( - CHANNEL_REPOSITORY, - UserFactory.getUserService(), - new ReadStatusInMemoryRepository(), - new InMemoryMessageRepository() - ); - - public static ChannelService getChannelService() { - return CHANNEL_SERVICE; - } - - public static ChannelRepository createChannelRepository() { - Properties properties = new Properties(); - try (InputStream is = ChannelFactory.class.getClassLoader().getResourceAsStream("application.properties")) { - properties.load(is); - } catch (IOException exception) { - System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); - } - String repositoryType = properties.getProperty("repository.type", "memory"); - - return switch (repositoryType) { - default -> new InMemoryChannelRepository(); - }; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java deleted file mode 100644 index 6c895346e..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/MessageFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.sprint.mission.discodeit.config; - -import com.sprint.mission.discodeit.application.service.interfaces.MessageService; -import com.sprint.mission.discodeit.application.service.message.JCFMessageService; -import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -public class MessageFactory { - - private static final MessageRepository MESSAGE_REPOSITORY = createMessageRepository(); - private static final MessageService MESSAGE_SERVICE = - new JCFMessageService(MESSAGE_REPOSITORY, ChannelFactory.getChannelService(), UserFactory.getUserService()); - - public static MessageService getMessageService() { - return MESSAGE_SERVICE; - } - - public static MessageRepository createMessageRepository() { - Properties properties = new Properties(); - try (InputStream is = ChannelFactory.class.getClassLoader().getResourceAsStream("application.properties")) { - properties.load(is); - } catch (IOException e) { - System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); - } - String repositoryType = properties.getProperty("repository.type", "memory"); - - return switch (repositoryType) { - default -> MESSAGE_REPOSITORY; - }; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java deleted file mode 100644 index cbbe58db9..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/config/UserFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sprint.mission.discodeit.config; - -import com.sprint.mission.discodeit.application.auth.PasswordEncoder; -import com.sprint.mission.discodeit.application.service.interfaces.UserService; -import com.sprint.mission.discodeit.application.service.user.JCFUserService; -import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; -import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; -import com.sprint.mission.discodeit.repository.user.InMemoryUserRepository; -import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; -import com.sprint.mission.discodeit.repository.userstatus.UserStatusInMemoryRepository; -import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -public class UserFactory { - - private static final UserRepository USER_REPOSITORY = createUserRepository(); - private static final UserService USER_SERVICE = - new JCFUserService( - USER_REPOSITORY, - new UserConverter(), - new PasswordEncoder(), - new UserStatusService(new UserStatusInMemoryRepository()) - ); - - public static UserService getUserService() { - return USER_SERVICE; - } - - public static UserRepository createUserRepository() { - Properties properties = new Properties(); - try (InputStream is = ChannelFactory.class.getClassLoader().getResourceAsStream("application.properties")) { - properties.load(is); - } catch (IOException exception) { - System.out.println("설정 파일 로드 실패, 기본값(memory) 사용"); - } - String repositoryType = properties.getProperty("repository.type", "memory"); - - return switch (repositoryType) { - default -> new InMemoryUserRepository(); - }; - } -} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties deleted file mode 100644 index 449ef48d0..000000000 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -repository.type = "memory" \ No newline at end of file diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml b/codeit-bootcamp-spring/1-sprint-mission/src/main/resources/application.yaml new file mode 100644 index 000000000..e69de29bb From 7cbfa3dcfd6d5cf318475fca41a8f490321e62c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:26:23 +0900 Subject: [PATCH 31/38] =?UTF-8?q?refactor:=20=EC=9D=BC=EA=B4=80=EB=90=9C?= =?UTF-8?q?=20=EC=BD=94=EB=94=A9=20=EC=8A=A4=ED=83=80=EC=9D=BC=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 --- .../mission/discodeit/JavaApplication.java | 2 +- .../service/channel/JCFChannelService.java | 4 +- .../service/user/JCFUserService.java | 10 +++- .../discodeit/domain/channel/Channel.java | 12 +++-- .../domain/channel/ParticipatedUser.java | 4 ++ .../mission/discodeit/domain/user/Email.java | 34 ++++++++++--- .../discodeit/domain/user/Nickname.java | 34 ++++++++++--- .../discodeit/domain/user/Password.java | 10 ++-- .../discodeit/domain/user/Username.java | 49 ++++++++++++++++--- .../user/validation/PasswordValidator.java | 1 + .../global/error/GlobalException.java | 3 +- ...ry.java => ChannelInMemoryRepository.java} | 8 +-- .../channel/FileChannelRepository.java | 3 +- .../channel/interfaces/ChannelRepository.java | 2 +- .../message/FileMessageRepository.java | 1 + ...ry.java => MessageInMemoryRepository.java} | 5 +- .../ReadStatusInMemoryRepository.java | 2 + ...ry.java => ChannelInMemoryRepository.java} | 2 +- 18 files changed, 149 insertions(+), 37 deletions(-) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/{InMemoryChannelRepository.java => ChannelInMemoryRepository.java} (81%) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/{InMemoryMessageRepository.java => MessageInMemoryRepository.java} (86%) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/{InMemoryUserRepository.java => ChannelInMemoryRepository.java} (96%) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java index a18709bd9..c25af5bc1 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/JavaApplication.java @@ -3,6 +3,6 @@ public class JavaApplication { public static void main(String[] args) { - } + } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index 5855789ed..f0d136571 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -23,7 +23,9 @@ import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +import org.springframework.stereotype.Service; +@Service public class JCFChannelService implements ChannelService { private final ChannelRepository channelRepository; @@ -87,7 +89,7 @@ public FoundChannelResponseDto findOneByChannelId(UUID channelId) { return toFoundChannelResponseDto(foundChannel); } - public List findAllByChannelId(UUID userId) { + public List findAllByUserId(UUID userId) { List channels = channelRepository.findAllByUserId(userId); // 채널 한 개당 메세지를 조회해오는 N + 1 return channels.stream().map(this::toFoundChannelResponseDto).toList(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java index c809e7a0e..9f7f53f1b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java @@ -5,9 +5,11 @@ import com.sprint.mission.discodeit.application.dto.user.LoginRequestDto; import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; +import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; +import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.user.BirthDate; import com.sprint.mission.discodeit.domain.user.Email; import com.sprint.mission.discodeit.domain.user.Nickname; @@ -21,6 +23,7 @@ import com.sprint.mission.discodeit.domain.user.validation.PasswordValidator; import com.sprint.mission.discodeit.domain.userstatus.UserStatus; import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.repository.channel.interfaces.ChannelRepository; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import java.util.List; import java.util.UUID; @@ -33,17 +36,20 @@ public class JCFUserService implements UserService { private final UserConverter userConverter; private final PasswordEncoder passwordEncoder; private final UserStatusService userStatusService; + private final ChannelRepository channelRepository; public JCFUserService( UserRepository userRepository, UserConverter userConverter, PasswordEncoder passwordEncoder, - UserStatusService userStatusService + UserStatusService userStatusService, + ChannelRepository channelRepository ) { this.userRepository = userRepository; this.userConverter = userConverter; this.passwordEncoder = passwordEncoder; this.userStatusService = userStatusService; + this.channelRepository = channelRepository; } @Override @@ -93,6 +99,8 @@ public void changePassword(UUID userId, ChangePasswordRequestDto requestDto) { @Override public void quitUser(UUID userId) { User foundUser = findOneByIdOrThrow(userId); + List channels = channelRepository.findAllByUserId(userId); + channels.forEach(channel -> channel.quitChannel(foundUser)); userStatusService.delete(foundUser); userRepository.deleteByUser(foundUser); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index c4fdd4630..cff1c5d7d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -67,10 +67,8 @@ public void updateName(String name) { this.name = name.trim(); } - private void validate(String name) { - if (Objects.isNull(name) || name.isBlank()) { - throw new ChannelNameInvalidException(ErrorCode.INVALID_CHANNEL_NAME_NOT_NULL, name); - } + public void quitChannel(User user) { + participatedUser.removeUser(user); } public boolean isManager(User user) { @@ -85,6 +83,12 @@ public Set getParticipantUserId() { return this.participatedUser.getParticipatedUserId(); } + private void validate(String name) { + if (Objects.isNull(name) || name.isBlank()) { + throw new ChannelNameInvalidException(ErrorCode.INVALID_CHANNEL_NAME_NOT_NULL, name); + } + } + public UUID getId() { return id; } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java index 1c7a7bd99..721ce8761 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java @@ -15,6 +15,10 @@ public void addUser(User user) { users.put(user.getId(), user); } + public void removeUser(User user) { + users.remove(user.getId()); + } + public Set getParticipatedUserId() { return Collections.unmodifiableSet(users.keySet()); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java index 9a6e65384..54d2f0e5f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Email.java @@ -4,13 +4,7 @@ import com.sprint.mission.discodeit.global.error.ErrorCode; import java.util.Objects; import java.util.regex.Pattern; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -@Getter -@EqualsAndHashCode(of = {"value"}) -@ToString(of = {"value"}) public class Email { private final static String EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z]{2,7})+$"; @@ -31,4 +25,32 @@ public void valid(String email) { throw new EmailInvalidException(ErrorCode.INVALID_EMAIL_FORMAT, email); } } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Email email = (Email) o; + return Objects.equals(value, email.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return "Email{" + + "value='" + value + '\'' + + '}'; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java index a03a5cf6a..acd5cc951 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Nickname.java @@ -5,13 +5,7 @@ import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -@Getter -@EqualsAndHashCode(of = {"value"}) -@ToString(of = {"value"}) public class Nickname { private static final int MAX_LENGTH = 32; @@ -63,4 +57,32 @@ private void throwContainForbiddenWord(String value) { } } } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Nickname nickname = (Nickname) o; + return Objects.equals(value, nickname.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return "Nickname{" + + "value='" + value + '\'' + + '}'; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java index e9362775d..29edea2d5 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Password.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.domain.user; -import lombok.ToString; - -@ToString(of = "value") public class Password { private String value; @@ -22,4 +19,11 @@ private void setValue(String value) { public String getValue() { return value; } + + @Override + public String toString() { + return "Password{" + + "value='" + value + '\'' + + '}'; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java index 8bebe8eef..c85b017d8 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/Username.java @@ -5,13 +5,7 @@ import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -@Getter -@EqualsAndHashCode(of = "value") -@ToString(of = "value") public class Username { private static final int MIN_LENGTH = 2; @@ -31,19 +25,60 @@ public Username(String value) { this.value = value.toLowerCase(); } - public void validate(final String username) { + public void validate(String username) { + throwIsNull(username); + throwInvalidLength(username); + throwInvalidPattern(username); + throwContainForbiddenWord(username); + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Username username = (Username) o; + return Objects.equals(value, username.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public String toString() { + return "Username{" + + "value='" + value + '\'' + + '}'; + } + + private void throwIsNull(String username) { if (Objects.isNull(username) || username.isBlank()) { throw new UserNameInvalidException(ErrorCode.USERNAME_REQUIRED, ""); } + } + private void throwInvalidLength(String username) { if (username.length() > MAX_LENGTH || username.length() < MIN_LENGTH) { throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_LENGTH, username); } + } + private void throwInvalidPattern(String username) { if (!VALID_USER_NAME_PATTERN.matcher(username).matches()) { throw new UserNameInvalidException(ErrorCode.INVALID_USERNAME_FORMAT, username); } + } + private void throwContainForbiddenWord(String username) { String lowerUsername = username.toLowerCase(); for (String word : FORBIDDEN_WORD) { if (lowerUsername.contains(word)) { diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java index 9b45f3949..46405f3f2 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/validation/PasswordValidator.java @@ -22,4 +22,5 @@ public static void validateOrThrow(String password) { throw new PassWordInvalidException(ErrorCode.WEAK_PASSWORD, password); } } + } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java index ad7c110aa..9b335939d 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/GlobalException.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; import com.sprint.mission.discodeit.global.error.exception.InvalidException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -28,7 +29,7 @@ protected ResponseEntity handleInvalidException(InvalidException protected ResponseEntity handleEntityNotFoundException(EntityNotFoundException exception) { ErrorResponse errorResponse = new ErrorResponse(exception.getErrorCode().getDescription(), exception.getErrorCode().getStatus()); - return ResponseEntity.status(404).body(errorResponse); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } @ExceptionHandler(Exception.class) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/ChannelInMemoryRepository.java similarity index 81% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/ChannelInMemoryRepository.java index 8aa30d5fe..a2c720baa 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/InMemoryChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/ChannelInMemoryRepository.java @@ -9,8 +9,10 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import org.springframework.stereotype.Repository; -public class InMemoryChannelRepository implements ChannelRepository { +@Repository +public class ChannelInMemoryRepository implements ChannelRepository { private final Map channelUUIDStore = new HashMap<>(); @@ -21,8 +23,8 @@ public Channel save(Channel channel) { } @Override - public Optional findOneById(UUID uuid) { - return Optional.ofNullable(channelUUIDStore.get(uuid)); + public Optional findOneById(UUID channelId) { + return Optional.ofNullable(channelUUIDStore.get(channelId)); } @Override diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java index 8242c22ca..1461392de 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/FileChannelRepository.java @@ -8,13 +8,14 @@ import java.util.UUID; public class FileChannelRepository implements ChannelRepository { + @Override public Channel save(Channel channel) { return null; } @Override - public Optional findOneById(UUID uuid) { + public Optional findOneById(UUID channelId) { return Optional.empty(); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java index 16df8f06e..7414bcd56 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/channel/interfaces/ChannelRepository.java @@ -10,7 +10,7 @@ public interface ChannelRepository { Channel save(Channel channel); - Optional findOneById(UUID uuid); + Optional findOneById(UUID channelId); List findAllByUserId(UUID userId); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java index c954da63b..ed7dc84d0 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/FileMessageRepository.java @@ -8,6 +8,7 @@ import java.util.UUID; public class FileMessageRepository implements MessageRepository { + @Override public Message save(Message message) { return null; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/MessageInMemoryRepository.java similarity index 86% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/MessageInMemoryRepository.java index 1bf305b08..cbfd7c8ac 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/InMemoryMessageRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/message/MessageInMemoryRepository.java @@ -6,8 +6,11 @@ import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; +import org.springframework.stereotype.Repository; + +@Repository +public class MessageInMemoryRepository implements MessageRepository { -public class InMemoryMessageRepository implements MessageRepository { @Override public Message save(Message message) { return null; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java index 2eff1677b..a2ab748e8 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/readstatus/ReadStatusInMemoryRepository.java @@ -9,7 +9,9 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; +import org.springframework.stereotype.Repository; +@Repository public class ReadStatusInMemoryRepository implements ReadStatusRepository { private final Map readStatusStore = new HashMap<>(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/ChannelInMemoryRepository.java similarity index 96% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/ChannelInMemoryRepository.java index 54e981a4a..2f9c79bef 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/InMemoryUserRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/ChannelInMemoryRepository.java @@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository; @Repository -public class InMemoryUserRepository implements UserRepository { +public class ChannelInMemoryRepository implements UserRepository { Map uuidUsers = new HashMap<>(); Map emailUsers = new HashMap<>(); From cc1d5767ea9eec1bc0bfd192405407f73309f851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:27:08 +0900 Subject: [PATCH 32/38] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sprint/mission/discodeit/fake/FakeFactory.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java index 18e324880..1ff5f6b61 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/FakeFactory.java @@ -5,9 +5,9 @@ import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; import com.sprint.mission.discodeit.fake.repository.FakeUserRepository; +import com.sprint.mission.discodeit.repository.channel.ChannelInMemoryRepository; import com.sprint.mission.discodeit.repository.user.interfaces.UserRepository; import com.sprint.mission.discodeit.repository.userstatus.UserStatusInMemoryRepository; -import com.sprint.mission.discodeit.repository.userstatus.interfaces.UserStatusRepository; public class FakeFactory { private FakeFactory() { @@ -22,6 +22,8 @@ public static JCFUserService getUserService() { getUserRepository(), new UserConverter(), new PasswordEncoder(), - new UserStatusService(new UserStatusInMemoryRepository())); + new UserStatusService(new UserStatusInMemoryRepository()), + new ChannelInMemoryRepository() + ); } } From ad5b7673e5911779176779dfbd20e3d457eb22e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:50:55 +0900 Subject: [PATCH 33/38] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=AC=B6=EA=B8=B0=20=3D=20createChannel?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C(public,=20p?= =?UTF-8?q?rivate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/channel/CreateChannelRequestDto.java | 4 +- .../dto/channel/FoundChannelResponseDto.java | 1 + .../service/channel/JCFChannelService.java | 55 +++++++++---------- .../service/interfaces/ChannelService.java | 8 +-- .../service/message/JCFMessageService.java | 3 +- .../service/readstatus/ReadStatusService.java | 2 +- .../discodeit/domain/channel/Channel.java | 8 ++- .../domain/userstatus/UserStatus.java | 3 +- 8 files changed, 44 insertions(+), 40 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java index eb690f48e..4a1cf6405 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/CreateChannelRequestDto.java @@ -1,10 +1,12 @@ package com.sprint.mission.discodeit.application.dto.channel; import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; +import com.sprint.mission.discodeit.domain.channel.enums.ChannelVisibility; import jakarta.validation.constraints.NotBlank; public record CreateChannelRequestDto( @NotBlank String name, - @NotBlank ChannelType channelType + @NotBlank ChannelType channelType, + @NotBlank ChannelVisibility visibility ) { } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java index ef8eea9ac..a04e9c7d6 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/dto/channel/FoundChannelResponseDto.java @@ -11,6 +11,7 @@ public record FoundChannelResponseDto( LocalDateTime lastMessageTime, Set participatedUserId ) { + public static FoundChannelResponseDto ofPublicChannel(Channel channel, LocalDateTime lastMessageTime) { return new FoundChannelResponseDto(channel.getId(), channel.getName(), lastMessageTime, Set.of()); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index f0d136571..e71c5fdc2 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -10,7 +10,6 @@ import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; -import com.sprint.mission.discodeit.domain.channel.enums.ChannelType; import com.sprint.mission.discodeit.domain.channel.enums.ChannelVisibility; import com.sprint.mission.discodeit.domain.channel.exception.AlreadyJoinUserException; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; @@ -47,45 +46,30 @@ public JCFChannelService( @Override public ChannelCreateResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); - ChannelType channelType = requestDto.channelType(); - Channel createChannel = new Channel(requestDto.name(), channelType, foundUser, ChannelVisibility.PUBLIC); - Channel savedChannel = channelRepository.save(createChannel); + Channel savedChannel = createChannelWithVisibility(userId, requestDto); return ChannelCreateResponseDto.from(savedChannel); } + // 같은 기능의 중복 메서드 => 요구사항에 두 개로 나누어 두라고 했지만, 로직이 매우 비슷. @Override - public ChannelCreateResponseDto createPrivateChannel(UUID userId) { - User foundUser = userService.findOneByIdOrThrow(userId); - Channel createChannel = Channel.ofPrivateChannel(foundUser, ChannelType.TEXT); - Channel savedChannel = channelRepository.save(createChannel); - ReadStatus readStatus = new ReadStatus(foundUser, savedChannel); - readStatusRepository.save(readStatus); + public ChannelCreateResponseDto createPrivateChannel(UUID userId, CreateChannelRequestDto requestDto) { + Channel savedChannel = createChannelWithVisibility(userId, requestDto); return ChannelCreateResponseDto.from(savedChannel); } @Override - public void joinPublicChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { + public void joinChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(invitedUserId); - Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); - foundChannel.join(foundUser); - channelRepository.save(foundChannel); - } - - @Override - public void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(invitedUserId); - Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsAlreadyJoinUser(foundUser, foundChannel); foundChannel.join(foundUser); - ReadStatus readStatus = new ReadStatus(foundUser, foundChannel); - readStatusRepository.save(readStatus); + createAndSaveReadStatus(foundUser, foundChannel); channelRepository.save(foundChannel); } @Override public FoundChannelResponseDto findOneByChannelId(UUID channelId) { - Channel foundChannel = findOneByIdOrThrow(channelId); + Channel foundChannel = findOneByChannelIdOrThrow(channelId); return toFoundChannelResponseDto(foundChannel); } @@ -96,7 +80,7 @@ public List findAllByUserId(UUID userId) { } @Override - public Channel findOneByIdOrThrow(UUID id) { + public Channel findOneByChannelIdOrThrow(UUID id) { return channelRepository.findOneById(id) .orElseThrow(() -> new ChannelNotFoundException(ErrorCode.NOT_FOUND)); } @@ -104,7 +88,7 @@ public Channel findOneByIdOrThrow(UUID id) { @Override public void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); foundChannel.updateSubject(requestDto.subject()); channelRepository.save(foundChannel); @@ -113,7 +97,7 @@ public void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto @Override public void changeChannelName(UUID userId, ChangeChannelNameRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); foundChannel.updateName(requestDto.channelName()); channelRepository.save(foundChannel); @@ -122,7 +106,7 @@ public void changeChannelName(UUID userId, ChangeChannelNameRequestDto requestDt @Override public void deleteChannel(UUID userId, DeleteChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - Channel foundChannel = findOneByIdOrThrow(requestDto.channelId()); + Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); readStatusRepository.deleteByChannel(foundChannel); messageRepository.deleteByChannel(foundChannel); @@ -149,4 +133,19 @@ private void throwIsAlreadyJoinUser(User targetUser, Channel targetChannel) { throw new AlreadyJoinUserException(ErrorCode.ALREADY_CHANNEL_JOIN_USER, targetUser.getNicknameValue()); } } + + private void createAndSaveReadStatus(User foundUser, Channel savedChannel) { + ReadStatus readStatus = new ReadStatus(foundUser, savedChannel); + readStatusRepository.save(readStatus); + } + + private Channel createChannelWithVisibility(UUID userId, CreateChannelRequestDto requestDto) { + User foundUser = userService.findOneByIdOrThrow(userId); + Channel createChannel = requestDto.visibility() == ChannelVisibility.PUBLIC ? + Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser) : + Channel.ofPrivateChannel(foundUser, requestDto.channelType()); + Channel savedChannel = channelRepository.save(createChannel); + createAndSaveReadStatus(foundUser, savedChannel); + return savedChannel; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java index a67d53b28..e9f32749c 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/ChannelService.java @@ -14,15 +14,13 @@ public interface ChannelService { ChannelCreateResponseDto createPublicChannel(UUID userId, CreateChannelRequestDto requestDto); - ChannelCreateResponseDto createPrivateChannel(UUID userId); + ChannelCreateResponseDto createPrivateChannel(UUID userId, CreateChannelRequestDto requestDto); - void joinPublicChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); - - void joinPrivateChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); + void joinChannel(UUID invitedUserId, InviteChannelRequestDto requestDto); FoundChannelResponseDto findOneByChannelId(UUID channelId); - Channel findOneByIdOrThrow(UUID uuid); + Channel findOneByChannelIdOrThrow(UUID uuid); void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index 88d2b0c7d..0864643c3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -13,7 +13,6 @@ import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; import com.sprint.mission.discodeit.repository.message.interfaces.MessageRepository; -import java.time.LocalDateTime; import java.util.UUID; public class JCFMessageService implements MessageService { @@ -35,7 +34,7 @@ public JCFMessageService( @Override public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { User sender = userService.findOneByIdOrThrow(requestDto.userId()); - Channel destinationChannel = channelService.findOneByIdOrThrow(requestDto.destinationChannelId()); + Channel destinationChannel = channelService.findOneByChannelIdOrThrow(requestDto.destinationChannelId()); Message createMessage = messageRepository.save(new Message(sender, destinationChannel, requestDto.content())); return MessageResponseDto.from(createMessage); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java index 33b390387..80937c91f 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java @@ -29,7 +29,7 @@ public ReadStatusService( public void updateLastReadTime(UUID userId, ReadStatusUpdateRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - Channel foundChannel = channelService.findOneByIdOrThrow(requestDto.channelId()); + Channel foundChannel = channelService.findOneByChannelIdOrThrow(requestDto.channelId()); ReadStatus readStatus = findOneByUserIdAndChannelId(foundUser, foundChannel).orElseGet(() -> new ReadStatus(foundUser, foundChannel)); readStatus.updateLastReadAt(Instant.now()); readStatusRepository.save(readStatus); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index cff1c5d7d..ff5fa5dc3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -29,7 +29,7 @@ public class Channel implements Serializable { private final ChannelVisibility visibility; private final ParticipatedUser participatedUser; - public Channel( + private Channel( String name, ChannelType type, User manager, @@ -47,6 +47,10 @@ public Channel( participatedUser = new ParticipatedUser(); } + public static Channel ofPublicChannel(String name, ChannelType type, User user) { + return new Channel(name, type, user, ChannelVisibility.PUBLIC); + } + public static Channel ofPrivateChannel(User manager, ChannelType type) { return new Channel("", type, manager, ChannelVisibility.PRIVATE); } @@ -84,7 +88,7 @@ public Set getParticipantUserId() { } private void validate(String name) { - if (Objects.isNull(name) || name.isBlank()) { + if (Objects.isNull(name)) { throw new ChannelNameInvalidException(ErrorCode.INVALID_CHANNEL_NAME_NOT_NULL, name); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java index be26baf53..f6c757a19 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/userstatus/UserStatus.java @@ -2,6 +2,7 @@ import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.domain.userstatus.enums.OnlineStatus; +import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Objects; @@ -33,7 +34,7 @@ public void updateLastAccessedAt() { } public OnlineStatus getOnlineStatus() { - if (lastAccessedAt.isAfter(Instant.now().minus(5, ChronoUnit.MINUTES))) { + if (Duration.between(createdAt, Instant.now()).toMinutes() <= 5L) { return OnlineStatus.ONLINE; } return OnlineStatus.OFFLINE; From 8fd15994ab809543b5f39b2f31d20866b323d6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:07:14 +0900 Subject: [PATCH 34/38] =?UTF-8?q?refactor:=20=EB=B6=84=EA=B8=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=82=BC=ED=95=AD=EC=97=B0=EC=82=B0?= =?UTF-8?q?=EC=9E=90,=20if=20=EB=AC=B8=20=EC=A7=88=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/channel/JCFChannelService.java | 17 +++++++++++++---- .../exception/NotChannelManagerException.java | 11 +++++++++++ .../discodeit/global/error/ErrorCode.java | 2 ++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/NotChannelManagerException.java diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index e71c5fdc2..9ad33bb94 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -13,6 +13,7 @@ import com.sprint.mission.discodeit.domain.channel.enums.ChannelVisibility; import com.sprint.mission.discodeit.domain.channel.exception.AlreadyJoinUserException; import com.sprint.mission.discodeit.domain.channel.exception.ChannelNotFoundException; +import com.sprint.mission.discodeit.domain.channel.exception.NotChannelManagerException; import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; import com.sprint.mission.discodeit.domain.user.User; import com.sprint.mission.discodeit.global.error.ErrorCode; @@ -115,7 +116,7 @@ public void deleteChannel(UUID userId, DeleteChannelRequestDto requestDto) { private void throwIsNotManager(User foundUser, Channel foundChannel) { if (!foundChannel.isManager(foundUser)) { - throw new IllegalArgumentException(); + throw new NotChannelManagerException(ErrorCode.CHANNEL_ADMIN_REQUIRED, foundUser.getUsernameValue()); } } @@ -141,9 +142,17 @@ private void createAndSaveReadStatus(User foundUser, Channel savedChannel) { private Channel createChannelWithVisibility(UUID userId, CreateChannelRequestDto requestDto) { User foundUser = userService.findOneByIdOrThrow(userId); - Channel createChannel = requestDto.visibility() == ChannelVisibility.PUBLIC ? - Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser) : - Channel.ofPrivateChannel(foundUser, requestDto.channelType()); + Channel createChannel = null; + if (requestDto.visibility() == ChannelVisibility.PUBLIC) { + createChannel = Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser); + } else if (requestDto.visibility() == ChannelVisibility.PRIVATE) { + createChannel = Channel.ofPrivateChannel(foundUser, requestDto.channelType()); + } +// ====> 삼항 연산자와 if 문 분기처리 중 어떤 것을 더 선호해야할까요? 제가 생각하기에는 삼항 연산자가 보기 더 편한것 같습니다. +// 다음 미션에서는 둘 중 하나 지워두겠습니다. +// createChannel = requestDto.visibility() == ChannelVisibility.PUBLIC ? +// Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser) : +// Channel.ofPrivateChannel(foundUser, requestDto.channelType()); Channel savedChannel = channelRepository.save(createChannel); createAndSaveReadStatus(foundUser, savedChannel); return savedChannel; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/NotChannelManagerException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/NotChannelManagerException.java new file mode 100644 index 000000000..ebadc2999 --- /dev/null +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/exception/NotChannelManagerException.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.domain.channel.exception; + +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.InvalidException; + +public class NotChannelManagerException extends InvalidException { + + public NotChannelManagerException(ErrorCode errorCode, String message) { + super(errorCode, message); + } +} diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java index 26003aa78..baa572534 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/ErrorCode.java @@ -30,6 +30,8 @@ public enum ErrorCode { // Channel INVALID_CHANNEL_NAME_NOT_NULL(400, "채널 이름은 필수 입력값 입니다."), ALREADY_CHANNEL_JOIN_USER(400, "이미 참여한 채널입니다."), + CHANNEL_ADMIN_REQUIRED(400, "채널 삭제가 허용되지 않은 사용자입니다."), + // Message INVALID_MESSAGE_CONTENT_NOT_NULL(400, "메시지 내용은 필수 입력값 입니다."), ; From d0231209ac3fb544247490059fd3493460b97135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:14:09 +0900 Subject: [PATCH 35/38] =?UTF-8?q?docs:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=82=AD=EC=A0=9C=20(=EC=BD=94=EB=93=9C=EC=97=90?= =?UTF-8?q?=20=EC=A7=91=EC=A4=91=ED=95=A0=20=EC=88=98=20=EC=9E=87=EB=8F=84?= =?UTF-8?q?=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1-sprint-mission/README.md | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/README.md b/codeit-bootcamp-spring/1-sprint-mission/README.md index 4e7efe866..e69de29bb 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/README.md +++ b/codeit-bootcamp-spring/1-sprint-mission/README.md @@ -1,102 +0,0 @@ -프로젝트 마일스톤 - -- Java 프로젝트를 Spring 프로젝트로 마이그레이션 -- 의존성 관리를 IoC Container에 위임하도록 리팩토링 -- 비즈니스 로직 고도화 - 기본 요구사항 - Spring 프로젝트 초기화 - - [x] Spring Initializr를 통해 zip 파일을 다운로드하세요. - - [x] 빌드 시스템은 Gradle - Groovy를 사용합니다. - - [x] 언어는 Java 17를 사용합니다. - - [x] Spring Boot의 버전은 3.4.0입니다. - - [x] GroupId는 com.sprint.mission입니다. - - [x] ArtifactId와 Name은 discodeit입니다. - - [x] packaging 형식은 Jar입니다 - - [x] Dependency를 추가합니다. - - [x] Lombok - - [x] Spring Web - - [x] zip 파일을 압축해제하고 원래 진행 중이던 프로젝트에 붙여넣기하세요. 일부 파일은 덮어쓰기할 수 있습니다. - - [x] application.properties 파일을 yaml 형식으로 변경하세요. - - [x] DiscodeitApplication의 main 메서드를 실행하고 로그를 확인해보세요. - - ---- - -Bean 선언 및 테스트 -- [x] File*Repository 구현체를 Repository 인터페이스의 Bean으로 등록하세요. -- [x] Basic*Service 구현체를 Service 인터페이스의 Bean으로 등록하세요. -- [x] JavaApplication에서 테스트했던 코드를 DiscodeitApplication에서 테스트해보세요. - - [x] JavaApplication 의 main 메소드를 제외한 모든 메소드를 DiscodeitApplication클래스로 복사하세요. - - [x] JavaApplication의 main 메소드에서 Service를 초기화하는 코드를 Spring Context를 활용하여 대체하세요. - -```java -// JavaApplication -public static void main(String[] args) { - // 레포지토리 초기화 - // ... - // 서비스 초기화 - UserService userService = new BasicUserService(userRepository); - ChannelService channelService = new BasicChannelService(channelRepository); - MessageService messageService = new BasicMessageService(messageRepository, channelRepository, userRepository); - - // ... -} - -// DiscodeitApplication -public static void main(String[] args) { - ConfigurableApplicationContext context = SpringApplication.run(DiscodeitApplication.class, args); - // 서비스 초기화 - // TODO context에서 Bean을 조회하여 각 서비스 구현체 할당 코드 작성하세요. - UserService userService; - ChannelService channelService; - MessageService messageService; - - // ... -} -``` - - - [x] JavaApplication의 main 메소드의 셋업, 테스트 부분의 코드를 DiscodeitApplication클래스로 복사하세요. -```java -public static void main(String[] args) { - // ... - // 셋업 - User user = setupUser(userService); - Channel channel = setupChannel(channelService); - // 테스트 - messageCreateTest(messageService, channel, user); -} -``` - -Spring 핵심 개념 이해하기 -- [ ] JavaApplication과 DiscodeitApplication에서 Service를 초기화하는 방식의 차이에 대해 다음의 키워드를 중심으로 정리해보세요. -IoC Container -Dependency Injection -Bean - - -Lombok 적용 -- [x] 도메인 모델의 getter 메소드를 @Getter로 대체해보세요. -- [x] Basic*Service의 생성자를 @RequiredArgsConstructor로 대체해보세요. - - -비즈니스 로직 고도화 -- [ ] 다음의 기능 요구 사항을 구현하세요. - -추가 기능 요구사항 -시간 타입 변경하기 -- [ ] 시간을 다루는 필드의 타입은 Instant로 통일합니다. - - 기존에 사용하던 Long보다 가독성이 뛰어나며, 시간대(Time Zone) 변환과 정밀한 시간 연산이 가능해 확장성이 높습니다. - -**새로운 도메인 추가하기** -- [ ] 공통: 앞서 정의한 도메인 모델과 동일하게 공통 필드(id, createdAt, updatedAt)를 포함합니다. -- [ ] ReadStatus - - 사용자가 채널 별 마지막으로 메시지를 읽은 시간을 표현하는 도메인 모델입니다. 사용자별 각 채널에 읽지 않은 메시지를 확인하기 위해 활용합니다. -- [ ] UserStatus - - 사용자 별 마지막으로 확인된 접속 시간을 표현하는 도메인 모델입니다. 사용자의 온라인 상태를 확인하기 위해 활용합니다. - - [ ] 마지막 접속 시간을 기준으로 현재 로그인한 유저로 판단할 수 있는 메소드를 정의하세요. 마지막 접속 시간이 현재 시간으로부터 5분 이내이면 현재 접속 중인 유저로 간주합니다. -- [ ] BinaryContent - - 이미지, 파일 등 바이너리 데이터를 표현하는 도메인 모델입니다. 사용자의 프로필 이미지, 메시지에 첨부된 파일을 저장하기 위해 활용합니다. - - [ ] 수정 불가능한 도메인 모델로 간주합니다. 따라서 updatedAt 필드는 정의하지 않습니다. - - [ ] User, Message 도메인 모델과의 의존 관계 방향성을 잘 고려하여 id 참조 필드를 추가하세요. -- [ ] 각 도메인 모델 별 레포지토리 인터페이스를 선언하세요. -레포지토리 구현체(File, JCF)는 아직 구현하지 마세요. 이어지는 서비스 고도화 요구사항에 따라 레포지토리 인터페이스에 메소드가 추가될 수 있어요. \ No newline at end of file From fef505579953fe6539d9be6cf5f79c7f357daefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:19:59 +0900 Subject: [PATCH 36/38] =?UTF-8?q?refactor:=20lombok=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1-sprint-mission/build.gradle | 4 ++-- .../application/service/user/JCFUserService.java | 16 ++++++++-------- .../mission/discodeit/domain/user/BirthDate.java | 5 +++-- .../mission/discodeit/domain/user/User.java | 2 -- .../error/exception/BusinessException.java | 2 -- .../discodeit/fake/domain/user/StubUser.java | 16 ++++++++-------- 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/build.gradle b/codeit-bootcamp-spring/1-sprint-mission/build.gradle index 6feb1dcd3..602a354c3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/build.gradle +++ b/codeit-bootcamp-spring/1-sprint-mission/build.gradle @@ -25,8 +25,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' +// compileOnly 'org.projectlombok:lombok' +// annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java index 9f7f53f1b..310051130 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java @@ -122,13 +122,13 @@ private boolean matchUserPassword(String rawPassword, String encodedPassword) { } private User toUserWithPasswordEncode(joinUserRequestDto requestDto) { - return User.builder() - .username(new Username(requestDto.username())) - .nickname(new Nickname(requestDto.nickname())) - .email(new Email(requestDto.email())) - .password(new Password(passwordEncoder.encode(requestDto.password()))) - .birthDate(new BirthDate(requestDto.birthDate())) - .emailSubscriptionStatus(EmailSubscriptionStatus.UNSUBSCRIBED) - .build(); + return new User( + new Nickname(requestDto.nickname()), + new Username(requestDto.username()), + new Email(requestDto.email()), + new Password(passwordEncoder.encode(requestDto.password())), + new BirthDate(requestDto.birthDate()), + EmailSubscriptionStatus.UNSUBSCRIBED + ); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java index a2dc412ca..f992b0c9b 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java @@ -4,9 +4,7 @@ import com.sprint.mission.discodeit.global.error.ErrorCode; import java.time.LocalDate; import java.util.Objects; -import lombok.Getter; -@Getter public class BirthDate { private static final int MIN_AGE_RESTRICT = 13; @@ -31,4 +29,7 @@ public void validate(LocalDate birthDate) { } } + public LocalDate getValue() { + return value; + } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java index 4825d73e9..a4e7d5150 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/User.java @@ -6,7 +6,6 @@ import java.time.Instant; import java.util.Objects; import java.util.UUID; -import lombok.Builder; public class User implements Serializable { @@ -23,7 +22,6 @@ public class User implements Serializable { private final Instant updatedAt; private final EmailSubscriptionStatus emailSubscriptionStatus; - @Builder public User( Nickname nickname, Username username, diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java index 917cf9764..6162e5457 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/global/error/exception/BusinessException.java @@ -1,9 +1,7 @@ package com.sprint.mission.discodeit.global.error.exception; import com.sprint.mission.discodeit.global.error.ErrorCode; -import lombok.Getter; -@Getter public class BusinessException extends RuntimeException { protected final ErrorCode errorCode; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java index 5e5614782..dd216d603 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/test/java/com/sprint/mission/discodeit/fake/domain/user/StubUser.java @@ -19,14 +19,14 @@ public class StubUser { private static final LocalDate BIRTH_DATE = LocalDate.of(2000, 1, 1); public static User generateUser() { - return User.builder() - .username(new Username(USER_NAME)) - .nickname(new Nickname(NICK_NAME)) - .email(new Email(EMAIL)) - .password(new Password(PASSWORD)) - .birthDate(new BirthDate(BIRTH_DATE)) - .emailSubscriptionStatus(EmailSubscriptionStatus.SUBSCRIBED) - .build(); + return new User( + new Nickname(USER_NAME), + new Username(USER_NAME), + new Email(EMAIL), + new Password(PASSWORD), + new BirthDate(BIRTH_DATE), + EmailSubscriptionStatus.UNSUBSCRIBED + ); } public static joinUserRequestDto generateJoinRequestDto() { From dfd4968a0125281672ce1a708638e6925a135aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:36:48 +0900 Subject: [PATCH 37/38] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=91=EA=B7=BC=20=EB=A1=9C=EC=A7=81=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(ReadStatus)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/channel/JCFChannelService.java | 16 ++++++++-------- .../service/interfaces/MessageService.java | 2 +- .../service/interfaces/UserService.java | 2 +- .../service/message/JCFMessageService.java | 19 ++++++++++++------- .../service/readstatus/ReadStatusService.java | 16 +++++----------- .../service/user/JCFUserService.java | 7 +++---- .../discodeit/domain/message/Message.java | 4 ++++ 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java index 9ad33bb94..d62313baa 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/channel/JCFChannelService.java @@ -60,7 +60,7 @@ public ChannelCreateResponseDto createPrivateChannel(UUID userId, CreateChannelR @Override public void joinChannel(UUID invitedUserId, InviteChannelRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(invitedUserId); + User foundUser = userService.findOneByUserIdOrThrow(invitedUserId); Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsAlreadyJoinUser(foundUser, foundChannel); foundChannel.join(foundUser); @@ -88,7 +88,7 @@ public Channel findOneByChannelIdOrThrow(UUID id) { @Override public void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); + User foundUser = userService.findOneByUserIdOrThrow(userId); Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); foundChannel.updateSubject(requestDto.subject()); @@ -97,7 +97,7 @@ public void changeSubject(UUID userId, ChangeChannelSubjectRequestDto requestDto @Override public void changeChannelName(UUID userId, ChangeChannelNameRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); + User foundUser = userService.findOneByUserIdOrThrow(userId); Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); foundChannel.updateName(requestDto.channelName()); @@ -106,7 +106,7 @@ public void changeChannelName(UUID userId, ChangeChannelNameRequestDto requestDt @Override public void deleteChannel(UUID userId, DeleteChannelRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); + User foundUser = userService.findOneByUserIdOrThrow(userId); Channel foundChannel = findOneByChannelIdOrThrow(requestDto.channelId()); throwIsNotManager(foundUser, foundChannel); readStatusRepository.deleteByChannel(foundChannel); @@ -141,7 +141,7 @@ private void createAndSaveReadStatus(User foundUser, Channel savedChannel) { } private Channel createChannelWithVisibility(UUID userId, CreateChannelRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); + User foundUser = userService.findOneByUserIdOrThrow(userId); Channel createChannel = null; if (requestDto.visibility() == ChannelVisibility.PUBLIC) { createChannel = Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser); @@ -150,9 +150,9 @@ private Channel createChannelWithVisibility(UUID userId, CreateChannelRequestDto } // ====> 삼항 연산자와 if 문 분기처리 중 어떤 것을 더 선호해야할까요? 제가 생각하기에는 삼항 연산자가 보기 더 편한것 같습니다. // 다음 미션에서는 둘 중 하나 지워두겠습니다. -// createChannel = requestDto.visibility() == ChannelVisibility.PUBLIC ? -// Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser) : -// Channel.ofPrivateChannel(foundUser, requestDto.channelType()); +// createChannel = requestDto.visibility() == ChannelVisibility.PUBLIC +// ? Channel.ofPublicChannel(requestDto.name(), requestDto.channelType(), foundUser) +// : Channel.ofPrivateChannel(foundUser, requestDto.channelType()); Channel savedChannel = channelRepository.save(createChannel); createAndSaveReadStatus(foundUser, savedChannel); return savedChannel; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java index d1a8b44ec..6346ece32 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/MessageService.java @@ -15,6 +15,6 @@ public interface MessageService { void deleteMessage(UUID userId, DeleteMessageRequestDto requestDto); - Message findOneByIdOrThrow(UUID uuid); + Message findOneByMessageIdOrThrow(UUID uuid); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java index 836c2bdb0..490da8df6 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/interfaces/UserService.java @@ -14,7 +14,7 @@ public interface UserService { void login(LoginRequestDto requestDto); - User findOneByIdOrThrow(UUID userId); + User findOneByUserIdOrThrow(UUID userId); List findAll(); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java index 0864643c3..d466224ad 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/message/JCFMessageService.java @@ -7,6 +7,7 @@ import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.MessageService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; +import com.sprint.mission.discodeit.application.service.readstatus.ReadStatusService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.message.Message; import com.sprint.mission.discodeit.domain.message.exception.MessageNotFoundException; @@ -20,20 +21,23 @@ public class JCFMessageService implements MessageService { private final MessageRepository messageRepository; private final ChannelService channelService; private final UserService userService; + private final ReadStatusService readStatusService; public JCFMessageService( MessageRepository messageRepository, ChannelService channelService, - UserService userService + UserService userService, + ReadStatusService readStatusService ) { this.messageRepository = messageRepository; this.channelService = channelService; this.userService = userService; + this.readStatusService = readStatusService; } @Override public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { - User sender = userService.findOneByIdOrThrow(requestDto.userId()); + User sender = userService.findOneByUserIdOrThrow(requestDto.userId()); Channel destinationChannel = channelService.findOneByChannelIdOrThrow(requestDto.destinationChannelId()); Message createMessage = messageRepository.save(new Message(sender, destinationChannel, requestDto.content())); return MessageResponseDto.from(createMessage); @@ -41,23 +45,24 @@ public MessageResponseDto createMessage(CreateMessageRequestDto requestDto) { @Override public void updateMessage(UUID userId, UpdateMessageContentRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); - Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); + User foundUser = userService.findOneByUserIdOrThrow(userId); + Message foundMessage = findOneByMessageIdOrThrow(requestDto.messageId()); throwIsNotSender(foundUser, foundMessage); foundMessage.updateContent(requestDto.content()); + readStatusService.updateLastReadTime(foundUser, foundMessage.getDestinationChannel()); messageRepository.save(foundMessage); } @Override public void deleteMessage(UUID userId, DeleteMessageRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); - Message foundMessage = findOneByIdOrThrow(requestDto.messageId()); + User foundUser = userService.findOneByUserIdOrThrow(userId); + Message foundMessage = findOneByMessageIdOrThrow(requestDto.messageId()); throwIsNotSender(foundUser, foundMessage); messageRepository.deleteById(foundMessage.getId()); } @Override - public Message findOneByIdOrThrow(UUID messageId) { + public Message findOneByMessageIdOrThrow(UUID messageId) { return messageRepository.findById(messageId) .orElseThrow(() -> new MessageNotFoundException(ErrorCode.NOT_FOUND)); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java index 80937c91f..c53b86a26 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/readstatus/ReadStatusService.java @@ -1,15 +1,14 @@ package com.sprint.mission.discodeit.application.service.readstatus; -import com.sprint.mission.discodeit.application.dto.readstatus.ReadStatusUpdateRequestDto; import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.domain.channel.Channel; import com.sprint.mission.discodeit.domain.readStatus.ReadStatus; import com.sprint.mission.discodeit.domain.user.User; +import com.sprint.mission.discodeit.global.error.ErrorCode; +import com.sprint.mission.discodeit.global.error.exception.EntityNotFoundException; import com.sprint.mission.discodeit.repository.readstatus.interfaces.ReadStatusRepository; import java.time.Instant; -import java.util.Optional; -import java.util.UUID; public class ReadStatusService { @@ -27,15 +26,10 @@ public ReadStatusService( this.channelService = channelService; } - public void updateLastReadTime(UUID userId, ReadStatusUpdateRequestDto requestDto) { - User foundUser = userService.findOneByIdOrThrow(userId); - Channel foundChannel = channelService.findOneByChannelIdOrThrow(requestDto.channelId()); - ReadStatus readStatus = findOneByUserIdAndChannelId(foundUser, foundChannel).orElseGet(() -> new ReadStatus(foundUser, foundChannel)); + public void updateLastReadTime(User user, Channel channel) { + ReadStatus readStatus = readStatusRepository.findOneByUserIdAndChannelId(user, channel) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_FOUND)); readStatus.updateLastReadAt(Instant.now()); readStatusRepository.save(readStatus); } - - public Optional findOneByUserIdAndChannelId(User foundUser, Channel foundChannel) { - return readStatusRepository.findOneByUserIdAndChannelId(foundUser, foundChannel); - } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java index 310051130..afa3820c3 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/application/service/user/JCFUserService.java @@ -5,7 +5,6 @@ import com.sprint.mission.discodeit.application.dto.user.LoginRequestDto; import com.sprint.mission.discodeit.application.dto.user.UserResponseDto; import com.sprint.mission.discodeit.application.dto.user.joinUserRequestDto; -import com.sprint.mission.discodeit.application.service.interfaces.ChannelService; import com.sprint.mission.discodeit.application.service.interfaces.UserService; import com.sprint.mission.discodeit.application.service.user.converter.UserConverter; import com.sprint.mission.discodeit.application.service.userstatus.UserStatusService; @@ -72,7 +71,7 @@ public void login(LoginRequestDto requestDto) { } @Override - public User findOneByIdOrThrow(UUID uuid) { + public User findOneByUserIdOrThrow(UUID uuid) { return userRepository.findOneById(uuid) .orElseThrow(() -> new UserNotFoundException(ErrorCode.NOT_FOUND)); } @@ -91,14 +90,14 @@ public List findAll() { @Override public void changePassword(UUID userId, ChangePasswordRequestDto requestDto) { PasswordValidator.validateOrThrow(requestDto.password()); - User foundUser = findOneByIdOrThrow(userId); + User foundUser = findOneByUserIdOrThrow(userId); foundUser.updatePassword(passwordEncoder.encode(requestDto.password())); userRepository.save(foundUser); } @Override public void quitUser(UUID userId) { - User foundUser = findOneByIdOrThrow(userId); + User foundUser = findOneByUserIdOrThrow(userId); List channels = channelRepository.findAllByUserId(userId); channels.forEach(channel -> channel.quitChannel(foundUser)); userStatusService.delete(foundUser); diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java index a479c0f46..7c8fcbedd 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/message/Message.java @@ -63,6 +63,10 @@ public UUID getDestinationChannelId() { return destinationChannel.getId(); } + public Channel getDestinationChannel() { + return destinationChannel; + } + @Override public boolean equals(Object o) { if (this == o) { From bf4d325a24d3399be6fc512b70c51affd79ed08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=B1=EC=9E=AC=EC=9A=B0?= <157946706+jaewoo9797@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:08:09 +0900 Subject: [PATCH 38/38] =?UTF-8?q?refactor:=20=EC=A4=84=EB=B0=94=EA=BF=88,?= =?UTF-8?q?=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sprint/mission/discodeit/domain/channel/Channel.java | 1 + .../mission/discodeit/domain/channel/ParticipatedUser.java | 2 +- .../com/sprint/mission/discodeit/domain/user/BirthDate.java | 1 + ...annelInMemoryRepository.java => UserInMemoryRepository.java} | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) rename codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/{ChannelInMemoryRepository.java => UserInMemoryRepository.java} (96%) diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java index ff5fa5dc3..5f962c8a7 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/Channel.java @@ -67,6 +67,7 @@ public void updateSubject(String subject) { this.subject = subject; } + public void updateName(String name) { this.name = name.trim(); } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java index 721ce8761..f11180227 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/channel/ParticipatedUser.java @@ -20,6 +20,6 @@ public void removeUser(User user) { } public Set getParticipatedUserId() { - return Collections.unmodifiableSet(users.keySet()); + return Set.copyOf(users.keySet()); } } diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java index f992b0c9b..d27ee4008 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/domain/user/BirthDate.java @@ -6,6 +6,7 @@ import java.util.Objects; public class BirthDate { + private static final int MIN_AGE_RESTRICT = 13; private final LocalDate value; diff --git a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/ChannelInMemoryRepository.java b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/UserInMemoryRepository.java similarity index 96% rename from codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/ChannelInMemoryRepository.java rename to codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/UserInMemoryRepository.java index 2f9c79bef..5d94547d8 100644 --- a/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/ChannelInMemoryRepository.java +++ b/codeit-bootcamp-spring/1-sprint-mission/src/main/java/com/sprint/mission/discodeit/repository/user/UserInMemoryRepository.java @@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository; @Repository -public class ChannelInMemoryRepository implements UserRepository { +public class UserInMemoryRepository implements UserRepository { Map uuidUsers = new HashMap<>(); Map emailUsers = new HashMap<>();