diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8af972cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore index 47b6eafa..fe9f09fb 100644 --- a/.gitignore +++ b/.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,12 +35,9 @@ bin/ ### VS Code ### .vscode/ - -### Mac OS ### -.DS_Store - ... .idea ... /src/main/generated/ -*.ser \ No newline at end of file +*.ser +.discodeit/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3fb9454f..4f599e2b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,21 +1,36 @@ plugins { - id 'java' + id 'java' + id 'org.springframework.boot' version '3.4.5' + 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() + mavenCentral() } dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' - implementation 'org.projectlombok:lombok:1.18.30' - annotationProcessor 'org.projectlombok:lombok:1.18.30' + 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 { - useJUnitPlatform() -} \ No newline at end of file +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832..9bbc975c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 64d23678..37f853b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Thu Apr 03 09:47:20 KST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..faf93008 100644 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# 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"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +133,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 +200,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, 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 +216,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/gradlew.bat b/gradlew.bat index 107acd32..9d21a218 100644 --- a/gradlew.bat +++ b/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/settings.gradle b/settings.gradle index 79373e36..2437dfb2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ -rootProject.name = '3-sprint-mission' - +rootProject.name = 'discodeit' diff --git a/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java b/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java new file mode 100644 index 00000000..b32d6abd --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java @@ -0,0 +1,355 @@ +package com.sprint.mission.discodeit; + +import com.sprint.mission.discodeit.dto.data.ChannelDto; +import com.sprint.mission.discodeit.dto.request.*; +import com.sprint.mission.discodeit.entity.*; +import com.sprint.mission.discodeit.service.ChannelService; +import com.sprint.mission.discodeit.service.MessageService; +import com.sprint.mission.discodeit.service.ReadStatusService; +import com.sprint.mission.discodeit.service.UserService; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +@SpringBootApplication +public class DiscodeitApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(DiscodeitApplication.class, args); + + // Spring Context에서 Bean 조회 + UserService userService = context.getBean(UserService.class); + ChannelService channelService = context.getBean(ChannelService.class); + MessageService messageService = context.getBean(MessageService.class); + ReadStatusService readStatusService = context.getBean(ReadStatusService.class); + + // === 유저 CRUD 테스트 === + //User test01 = userService.createUser("test01", "test01@.com"); + User test01 = userService.createUser(new UserCreateRequest("test01", "test01@.com","1234"), Optional.empty()); + + UserCreateRequest test02userRequest = new UserCreateRequest("test02", "test02@.com","12345"); + byte[] content = {123}; // 파일 바이트 + BinaryContentCreateRequest test02profileRequest = new BinaryContentCreateRequest("123.jpg", content, "image/jpeg"); + User test02 = userService.createUser(test02userRequest, Optional.of(test02profileRequest)); + + //User test03 = userService.createUser("test03", "test03@.com"); + User test03 = userService.createUser(new UserCreateRequest("test03", "test03@.com","1234"), Optional.empty()); + + + userService.find(test01.getId()).ifPresentOrElse(userDTO -> { + System.out.println("===개별 User 조회 ===\n" + userDTO); + + System.out.println("\n=== test01 이름변경 + 프로필이미지 추가 후 조회 ==="); + //userService.updateUserName(userDTO.getId(), "test0101"); + UserUpdateRequest updateRequest = new UserUpdateRequest("test0101", "test01@.com"); + byte[] newImageContent = {42, 24, 55}; // 새로운 이미지 바이트 + BinaryContentCreateRequest profileImageRequest = + new BinaryContentCreateRequest("profile_test01.jpg", newImageContent, "image/jpeg"); + userService.update(test01.getId(), updateRequest, Optional.of(profileImageRequest)); + + //System.out.println(userService.find(userDTO.getId()).orElse(null)); + System.out.println(userService.find(test01.getId()).orElse(null)); + }, () -> { + System.out.println("test01 유저를 찾을 수 없습니다."); + }); + + System.out.println("\n=== 전체 유저 조회 ==="); + userService.findAll().forEach(System.out::println); + + // test02 삭제 및 조회 + userService.delete(test02.getId()); + System.out.println("\n=== test02 삭제 후 전체 조회 ==="); + userService.findAll().forEach(System.out::println); + System.out.println(); + + System.out.println("==============================================================================================="); + + // test01이 public채널 생성 & test03이 private채널 생성 + //Channel channel1 = channelService.create("2025_Channel", test01); + PublicChannelCreateRequest test01publicRequest = new PublicChannelCreateRequest("2025_Channel", test01.getId()); + Channel channel1 = channelService.create(test01publicRequest); + + //Channel channel2 = channelService.create("2024_Channel", test02); + List memberIds = new ArrayList<>(); + memberIds.add(test01.getId()); + PrivateChannelCreateRequest privateRequest = new PrivateChannelCreateRequest("2024_Channel", test03.getId(), memberIds); + Channel channel2 = channelService.create(privateRequest); + + // 개별 채널 조회 + System.out.println("\n=== 개별 채널 조회 ==="); + channelService.find(channel2.getId()).ifPresentOrElse( + channel -> System.out.println(channel), + () -> System.out.println("해당 채널이 존재하지 않습니다.") + ); + + System.out.println("\n=== 채널 이름(2024_Channel) 수정 후 전체 채널 조회 ==="); + PublicChannelUpdateRequest updateRequest = new PublicChannelUpdateRequest("2023_channel"); + channelService.update(channel2.getId(), updateRequest); + try { + channelService.update(channel2.getId(), updateRequest); + } catch (IllegalArgumentException e) { + System.out.println("[채널명 변경 실패: " + e.getMessage() + "]\n"); + } + List allChannels = channelService.findAllByUserId(test01.getId()); + allChannels.forEach(channelDto -> System.out.println(channelDto)); + + // 유저 추가 (test02, test03을 채널에 추가) & 삭제된 유저는 data에 담을 수 없다. + System.out.println("\n=== 2025_Channel에 멤버(test02(탈퇴),test03) 추가 ==="); + try { + channelService.addMember(channel1.getId(), test02.getId()); // test02는 삭제된 유저 + } catch (IllegalArgumentException e) { + System.out.println("예외 발생: " + e.getMessage()); + } + channelService.addMember(channel1.getId(), test03.getId()); + + // 채널 멤버 조회 + System.out.println("\n=== 2025_Channel 전체멤버(채널소유자 포함) 조회 ==="); + List channelMembers = channelService.getChannelMembers(channel1.getId()); + channelMembers.forEach(user -> System.out.println("멤버: " + user.getUsername())); + + // 유저 제거 테스트 (test03 제거) + System.out.println("\n=== 2025_Channel에서 멤버 제거(test03) 후 조회 ==="); + channelService.removeMember(channel1.getId(), test03.getId()); + channelService.getChannelMembers(channel1.getId()).forEach(user -> System.out.println("멤버: " + user.getUsername())); + + // 채널 삭제 후 전체 채널 조회 + System.out.println("\n=== 2025_Channel 삭제 후 남은 채널 조회 ==="); + channelService.deleteChannel(channel1.getId()); + List totalChannels = channelService.findAllByUserId(test01.getId()); + if (totalChannels.isEmpty()) { + System.out.println("남은 채널 없음"); + } else { + totalChannels.forEach(channel -> System.out.println(channel)); + } + + System.out.println("\n============================================\n[##유저+채널+메시지 crud 테스트]====================\n"); + + User user1 = userService.createUser( + new UserCreateRequest("user1", "test01@.abc", "1234"), + Optional.empty() + ); + + byte[] content3 = {123,68,12}; + User user2 = userService.createUser( + new UserCreateRequest("user2", "test02@.abc", "12345"), + Optional.of(new BinaryContentCreateRequest("123.jpg", content3, "image/jpeg")) + ); + + User user3 = userService.createUser( + new UserCreateRequest("user3", "test03@.abc", "asd"), + Optional.empty() + ); + + User user4 = userService.createUser( + new UserCreateRequest("user4", "test04@.abc", "134"), + Optional.empty() + ); + + + //Channel studyRoom = channelService.createChannel("공부방", user1); + PublicChannelCreateRequest user1publicRequest = new PublicChannelCreateRequest("공부방", user1.getId()); + + Channel studyRoom = channelService.create(user1publicRequest); + channelService.addMember(studyRoom.getId(), user2.getId()); + channelService.addMember(studyRoom.getId(), user3.getId()); + channelService.addMember(studyRoom.getId(), user4.getId()); + + //Channel noticeRoom = channelService.createChannel("공지방", user2); + List member_Ids = List.of(user1.getId(), user3.getId(), user4.getId()); + PrivateChannelCreateRequest private_Request = new PrivateChannelCreateRequest("공지방", user2.getId(), member_Ids); + + Channel noticeRoom = channelService.create(private_Request); + + + byte[] content1 = {1, 2, 3}; + byte[] content2 = {4, 5, 6}; + BinaryContentCreateRequest file1 = new BinaryContentCreateRequest("123.jpg", content1, "image/jpeg"); + BinaryContentCreateRequest file2 = new BinaryContentCreateRequest("456.pdf", content2, "pdf"); + + messageService.createMessage( + new MessageCreateRequest(studyRoom.getId(), user1.getId(), "안녕하세요 저는 user1입니다. 첨부파일 0개."), + null + ); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + messageService.createMessage( + new MessageCreateRequest(studyRoom.getId(), user2.getId(), "user2 입니다. 첨부파일 1개"), + List.of(file1) + ); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + messageService.createMessage( + new MessageCreateRequest(studyRoom.getId(), user3.getId(), "user3 입니다. 첨부파일 2개"), + List.of(file1, file2) + ); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + messageService.createMessage( + new MessageCreateRequest(noticeRoom.getId(), user4.getId(), "안녕하세요 저는 user4입니다. 첨부파일 0개."), + null + ); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + messageService.createMessage( + new MessageCreateRequest(noticeRoom.getId(), user1.getId(), "공지없습니다. 첨부파일 0개."), + null + ); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + + System.out.println("[## user1의 readStatus 수정 전 lastReadAt 조회]"); // lastReadAt 기본값을 Instant.EPOCH로 설정 + List user1StatusesBeforeUpdate = readStatusService.findAllByUserId(user1.getId()); + for (ReadStatus rs : user1StatusesBeforeUpdate) { + if (rs.getChannelId().equals(noticeRoom.getId())) { + System.out.println("user1의 lastReadAt마지막 읽은 시간: " + rs.getLastReadAt()); + } + } + + List user1Statuses = readStatusService.findAllByUserId(user1.getId()); + for (ReadStatus rs : user1Statuses) { + if (rs.getChannelId().equals(noticeRoom.getId())) { + readStatusService.update(rs.getId(), new ReadStatusUpdateRequest(Instant.now())); + } + } + + System.out.println("\n[## user1의 readStatus 수정 후 lastReadAt 조회]"); + List user1StatusesAfterUpdate = readStatusService.findAllByUserId(user1.getId()); + for (ReadStatus rs : user1StatusesAfterUpdate) { + if (rs.getChannelId().equals(noticeRoom.getId())) { + System.out.println("마지막 읽은 시간: " + rs.getLastReadAt()); + } + } + + System.out.println("\n[##user1의 readStatus 조회 및 마지막으로 읽은 메시지 출력]"); + + List user1ReadStatuses = readStatusService.findAllByUserId(user1.getId()); + + for (ReadStatus rs : user1ReadStatuses) { + UUID channelId = rs.getChannelId(); + Optional optionalChannel = channelService.find(channelId); + + if (optionalChannel.isEmpty()) continue; + + ChannelDto channelDto = optionalChannel.get(); + + if (channelDto.channelType() != ChannelType.PRIVATE) continue; + + Instant lastReadAt = rs.getLastReadAt(); + String channelName = channelDto.name(); + + List messages = messageService.findAllByChannelId(channelId).stream() + .filter(m -> !m.getCreatedAt().isAfter(lastReadAt)) + .sorted(Comparator.comparing(Message::getCreatedAt).reversed()) + .collect(Collectors.toList()); + + if (!messages.isEmpty()) { + Message latestReadMessage = messages.get(0); + String formatted = messageService.formatMessage(latestReadMessage); + System.out.println("[" + channelName + "] " + formatted); + } else { + System.out.println("[" + channelName + "] 읽은 메시지가 없습니다."); + } + } + + + // === 메시지 기능 테스트 === + + //공지방에서 user1이 보낸 메시지 조회 + System.out.println("\n[##공지방에서 user1이 보낸 메시지]"); + messageService.getMessagesBySenderInChannel(noticeRoom.getId(), user1.getId()) + .forEach(msg -> System.out.println(messageService.formatMessage(msg))); + + //공부방에서 user4가 받은 메시지 조회 + System.out.println("\n[##공부방에서 user4가 받은 메시지]"); + messageService.getMessagesByReceiverInChannel(studyRoom.getId(), user4.getId()) + .forEach(msg -> System.out.println(messageService.formatMessage(msg))); + + System.out.println("\n[##user3 탈퇴 처리]"); + userService.delete(user3.getId()); + + //공지방 전체 메시지 조회 (user3가 요청자라고 가정) + System.out.println("\n[##공지방 전체 메시지 조회]"); + messageService.findAllByChannelId(noticeRoom.getId()) + .forEach(msg -> System.out.println(messageService.formatMessage(msg))); + + System.out.println("\n[##user1이 공부방에서 보낸 메시지 수정]"); + List messages = messageService.getMessagesBySenderInChannel(studyRoom.getId(), user1.getId()); + + if (!messages.isEmpty()) { + UUID messageId = messages.get(0).getId(); // 첫 번째 메시지를 대상으로 수정 + + // 수정 전 메시지 조회 + messageService.getMessageById(messageId).ifPresent(originalMessage -> { + String oldContent = originalMessage.getContent(); // 수정 전 내용 + + // 메시지 수정 + messageService.updateMessage( + messageId, new MessageUpdateRequest(user1.getId(), "안녕하세요 저는 user1입니다.(수정)") + ); + + // 수정 후 메시지 조회 + messageService.getMessageById(messageId).ifPresent(updatedMessage -> { + System.out.println("메시지 수정 완료"); + System.out.println("수정 전: \"" + oldContent + "\""); + System.out.println("수정 후: \"" + updatedMessage.getContent() + "\""); + }); + }); + } else { + System.out.println("수정할 메시지가 없습니다."); + } + + System.out.println("\n[##user4가 공지방에서 보낸 메시지 삭제]"); + List user4Msgs = messageService.getMessagesBySenderInChannel(noticeRoom.getId(), user4.getId()); + if (!user4Msgs.isEmpty()) { + UUID msgId = user4Msgs.get(0).getId(); + messageService.deleteMessage(msgId, user4.getId()); + System.out.println("삭제 완료"); + } + + System.out.println("\n[##user2가 공부방에서 받은 메시지 조회]"); + messageService.getMessagesByReceiverInChannel(studyRoom.getId(), user2.getId()) + .forEach(msg -> System.out.println(messageService.formatMessage(msg))); + + System.out.println("\n[##user2 탈퇴 처리 - 공지방 소유자]"); + userService.delete(user2.getId()); + + System.out.println("\n[##공지방 채널소유주인 user2가 탈퇴후 공지방 채널 메시지 조회 시도]"); + try { + List selectMsg = messageService.findAllByChannelId(noticeRoom.getId()); + selectMsg.forEach(msg -> System.out.println(messageService.formatMessage(msg))); + } catch (IllegalArgumentException e) { + System.out.println("예외 발생: " + e.getMessage()); // 예상: 채널이 존재하지 않습니다. + } catch (SecurityException e) { + System.out.println("예외 발생: " + e.getMessage()); // 예상: 채널에 접근할 수 있는 권한이 없습니다. + } + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/JavaApplication.java b/src/main/java/com/sprint/mission/discodeit/JavaApplication.java index a07ca760..2d7e2c47 100644 --- a/src/main/java/com/sprint/mission/discodeit/JavaApplication.java +++ b/src/main/java/com/sprint/mission/discodeit/JavaApplication.java @@ -17,9 +17,6 @@ import com.sprint.mission.discodeit.service.basic.BasicChannelService; import com.sprint.mission.discodeit.service.basic.BasicMessageService; import com.sprint.mission.discodeit.service.basic.BasicUserService; -import com.sprint.mission.discodeit.service.factory.ServiceFactory; -import com.sprint.mission.discodeit.service.file.FileMessageService; -import com.sprint.mission.discodeit.service.jcf.JcfMessageService; import java.util.HashMap; import java.util.List; @@ -28,6 +25,7 @@ public class JavaApplication { public static void main(String[] args) { + /* // JCF*Repository 구현체를 활용하여 테스트 // UserRepository userRepo = new JcfUserRepository(); // ChannelRepository channelRepo = new JcfChannelRepository(); @@ -248,5 +246,7 @@ public static void main(String[] args) { } catch (SecurityException e) { System.out.println("예외 발생: " + e.getMessage()); // 예상: 채널에 접근할 수 있는 권한이 없습니다. } - } + + */ + } } \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java b/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java new file mode 100644 index 00000000..5ca2f8cc --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.dto.data; + +import com.sprint.mission.discodeit.entity.ChannelType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public record ChannelDto( + UUID id, + String name, + UUID ownerId, + ChannelType channelType, + List memberIds, + LocalDateTime lastMessageTime +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java b/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java new file mode 100644 index 00000000..1ea18176 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java @@ -0,0 +1,32 @@ +package com.sprint.mission.discodeit.dto.data; + +import com.sprint.mission.discodeit.entity.User; +import lombok.Getter; + +import java.util.UUID; + +@Getter +public class UserDto { + private final UUID id; + private final String username; + private final String email; + private final boolean hasProfileImage; + private final boolean isOnline; + + public UserDto(User user, boolean hasProfileImage, boolean isOnline) { + this.id = user.getId(); + this.username = user.getUsername(); + this.email = user.getEmail(); + this.hasProfileImage = hasProfileImage; + this.isOnline = isOnline; + } + + @Override + public String toString() { + return "[" + "username='" + username + "', email='" + email + + "', 프로필이미지: " + hasProfileImage + + ", 온라인: " + isOnline + "]"; + } +} + + diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java new file mode 100644 index 00000000..d6e63eea --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.dto.request; + +public record BinaryContentCreateRequest( + String fileName, // 예: "123.jpg" + byte[] content, // 실제 파일 데이터 (저장소에 저장할 때 필요) + String fileType // 예: "image/jpeg" +){ + public boolean isValid() { + return fileName != null && !fileName.isBlank() + && content != null && content.length > 0 + && fileType != null && !fileType.isBlank(); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java new file mode 100644 index 00000000..a16e0d05 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java @@ -0,0 +1,4 @@ +package com.sprint.mission.discodeit.dto.request; + +public record LoginRequest(String username, String password) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java new file mode 100644 index 00000000..f9e0d060 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.UUID; + +public record MessageCreateRequest(UUID channelId, UUID senderId, String content) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java new file mode 100644 index 00000000..f43735bd --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.UUID; + +public record MessageUpdateRequest(UUID senderId, String newContent) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java new file mode 100644 index 00000000..ac9b7d0f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java @@ -0,0 +1,8 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.List; +import java.util.UUID; + +public record PrivateChannelCreateRequest( + String channelName, UUID ownerUserId, List memberIds) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java new file mode 100644 index 00000000..43cb4c2b --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.UUID; + +public record PublicChannelCreateRequest(String channelName, UUID ownerUserId) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java new file mode 100644 index 00000000..b1c1d069 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java @@ -0,0 +1,4 @@ +package com.sprint.mission.discodeit.dto.request; + +public record PublicChannelUpdateRequest(String channelName) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java new file mode 100644 index 00000000..e7352f1c --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java @@ -0,0 +1,5 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.UUID; + +public record ReadStatusCreateRequest(UUID userId, UUID channelId) {} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java new file mode 100644 index 00000000..2381d6cd --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java @@ -0,0 +1,5 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.time.Instant; + +public record ReadStatusUpdateRequest(Instant newLastReadAt) {} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java new file mode 100644 index 00000000..0d8815f4 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java @@ -0,0 +1,5 @@ +package com.sprint.mission.discodeit.dto.request; + +public record UserCreateRequest(String username, String email, String password) { + +} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java new file mode 100644 index 00000000..13443ca5 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.UUID; + +public record UserStatusCreateRequest(UUID id) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java new file mode 100644 index 00000000..8e244dde --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java @@ -0,0 +1,6 @@ +package com.sprint.mission.discodeit.dto.request; + +import java.util.UUID; + +public record UserStatusUpdateRequest(UUID id) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java new file mode 100644 index 00000000..919c5b95 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java @@ -0,0 +1,5 @@ +package com.sprint.mission.discodeit.dto.request; + +public record UserUpdateRequest(String username, String email) { + +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java new file mode 100644 index 00000000..f9c0aa31 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java @@ -0,0 +1,40 @@ +package com.sprint.mission.discodeit.entity; + +import lombok.Getter; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Getter +public class BinaryContent implements Serializable { + private static final long serialVersionUID = 1L; + private final UUID id; + private final Instant createdAt; + + private String fileName; // 파일 이름 (예: "123.png", "456.pdf") + private UUID userId; // 프로필 이미지와 관련된 유저 ID + private UUID messageId; // 메시지와 관련된 파일 (선택적) + + public BinaryContent(String fileName, UUID userId) { + this.id = UUID.randomUUID(); + this.createdAt = Instant.now(); + this.fileName = fileName; + this.userId = userId; + } + + public BinaryContent(String fileName, UUID userId, UUID messageId) { + this(fileName, userId); + this.messageId = messageId; + } + + @Override + public String toString() { + return "BinaryContent{" + + "fileName='" + fileName + '\'' + + ", userId=" + userId + + ", messageId=" + messageId + + '}'; + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java index 16cfbb1c..5bcb85b3 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java @@ -3,6 +3,7 @@ import lombok.Getter; import java.io.Serializable; +import java.time.Instant; import java.util.List; import java.util.UUID; @@ -11,40 +12,36 @@ public class Channel implements Serializable { private static final long serialVersionUID = 1L; private final UUID id; - private final Long createdAt; - private Long updatedAt; + private final Instant createdAt; + private Instant updatedAt; private String channelName; private User channelOwner; - private List channelUsers; + private List channelMembers; + private ChannelType channelType; - public Channel(String channelName, User channelOwner, List channelUsers) { + public Channel(String channelName, User channelOwner, List channelUsers, ChannelType channelType) { this.id = UUID.randomUUID(); - this.createdAt = System.currentTimeMillis(); - this.updatedAt = System.currentTimeMillis(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); this.channelName = channelName; this.channelOwner = channelOwner; - this.channelUsers = channelUsers; + this.channelMembers = channelUsers; + this.channelType = channelType; } public void updateChannelName(String channelName) { this.channelName = channelName; - this.updatedAt = System.currentTimeMillis(); + this.updatedAt = Instant.now(); } -// public void updateChannelOwner(User channelOwner) { -// this.channelOwner = channelOwner; -// this.updatedAt = System.currentTimeMillis(); -// // 예외: 채널에 포함된 멤버가 아니라면 채널소유자가 될 수 없다. -// } - public void addChannelUser(User user) { - this.channelUsers.add(user); + this.channelMembers.add(user); // 존재하는 유저인지? } public void removeChannelUser(User user) { - this.channelUsers.remove(user); + this.channelMembers.remove(user); // 채널에 포함된 유저인지? or 채널 소유자 인지? 채널소유자는 본인의 채널이 삭제되기전에는 채널을 나갈 수 없다. } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java b/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java new file mode 100644 index 00000000..f28ad94f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java @@ -0,0 +1,5 @@ +package com.sprint.mission.discodeit.entity; + +public enum ChannelType { + PUBLIC, PRIVATE; +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Message.java b/src/main/java/com/sprint/mission/discodeit/entity/Message.java index d9c03a8e..b2c3bc4e 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Message.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Message.java @@ -3,6 +3,9 @@ import lombok.Getter; import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; @Getter @@ -10,30 +13,38 @@ public class Message implements Serializable { private static final long serialVersionUID = 1L; private final UUID id; - private final Long createdAt; - private Long updatedAt; + private final Instant createdAt; + private Instant updatedAt; private String content; private UUID channelId; private UUID senderId; + private List attachmentIds; - public Message( String content,UUID channelId, UUID senderId) { + public Message(String content, UUID channelId, UUID senderId) { this.id = UUID.randomUUID(); - this.createdAt = System.currentTimeMillis(); - this.updatedAt = System.currentTimeMillis(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); this.content = content; this.channelId = channelId; this.senderId = senderId; + this.attachmentIds = new ArrayList<>(); + } + + // 첨부파일 추가 + public void addAttachment(UUID attachmentId) { + this.attachmentIds.add(attachmentId); // 첨부파일 리스트에 추가 } public void updateContent(String content) { this.content = content; - this.updatedAt = System.currentTimeMillis(); + this.updatedAt = Instant.now(); } @Override public String toString() { return "Message{" + + "id=" + id + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + ", content='" + content + '\'' + diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java new file mode 100644 index 00000000..dfd3c58f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java @@ -0,0 +1,42 @@ +package com.sprint.mission.discodeit.entity; + +import lombok.Getter; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Getter +public class ReadStatus implements Serializable { + private static final long serialVersionUID = 1L; + private final UUID id; + private final Instant createdAt; + private Instant updatedAt; + + private UUID userId; + private UUID channelId; + private Instant lastReadAt; + + public ReadStatus(UUID userId, UUID channelId) { + this.id = UUID.randomUUID(); + this.createdAt = Instant.now() ; + this.updatedAt = Instant.now(); + this.userId = userId; + this.channelId = channelId; + this.lastReadAt = Instant.EPOCH; // 기본값을 Instant.EPOCH로 메시지를 읽지 않음을 표시 + } + + /* + 사용자가 채널 별 마지막 메세지를 읽은 시간을 표현 + 마지막으로 읽은 시간 업데이트 필요 + 사용자가 각 채널에 읽지 않은 메세지를 확인위한 용도 + updateLastReadAt 호출 시점? 사용자가 채널에 접속할 때 + */ + public void updateLastReadAt(Instant currentReadAt) { + if (currentReadAt.isAfter(this.lastReadAt)) { + this.lastReadAt = currentReadAt; + this.updatedAt = Instant.now(); + } + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/entity/User.java b/src/main/java/com/sprint/mission/discodeit/entity/User.java index 94388e3b..8e7c072e 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/User.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/User.java @@ -1,10 +1,7 @@ package com.sprint.mission.discodeit.entity; import java.io.Serializable; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import java.time.Instant; import java.util.UUID; import lombok.Getter; @@ -13,31 +10,49 @@ public class User implements Serializable { private static final long serialVersionUID = 1L; private final UUID id; - private final Long createdAt; - private Long updatedAt; + private final Instant createdAt; + private Instant updatedAt; private String username; private String email; + private UUID profileId; + private String password; - public User(String username, String email) { + public User(String username, String email, String password) { this.id = UUID.randomUUID(); - this.createdAt = System.currentTimeMillis(); - this.updatedAt = System.currentTimeMillis(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); this.username = username; this.email = email; + this.password = password; + } + + public User(String username, String email) { + this.id = UUID.randomUUID(); + this.createdAt = Instant.now(); + this.updatedAt = Instant.now(); + this.username = username; + this.email = email; + this.password = null; } + public void updateName(String username) { this.username = username; - updatedAt = System.currentTimeMillis(); + updatedAt = Instant.now(); } public void updateEmail(String email) { this.email = email; - updatedAt = System.currentTimeMillis(); + updatedAt = Instant.now(); } +// public void setProfileId(UUID profileId) { +// this.profileId = profileId; +// this.updatedAt = Instant.now(); +// } + @Override public String toString() { return "[" + "username='" + username + ", email='" + email + '\'' + ']'; diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java new file mode 100644 index 00000000..0ace5e18 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java @@ -0,0 +1,39 @@ +package com.sprint.mission.discodeit.entity; + +import lombok.Getter; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Getter +public class UserStatus implements Serializable { + private static final long serialVersionUID = 1L; + private final UUID id; // UserStatus의 고유ID + private final Instant createdAt; + private Instant updatedAt; + + private UUID userId; // 온/오프 상태에 속한 사용자ID - User도메인 + + public UserStatus(UUID userId) { + this.id = UUID.randomUUID(); + this.createdAt =Instant.now() ; + this.updatedAt = Instant.now(); + this.userId = userId; + } + + // 사용자가 로그인 -> 서비스에서 호출 + public void updateTimestamp() { + this.updatedAt = Instant.now(); + } + + /* + 사용자가 로그인을 하면 마지막 접속 시간은 로그인을 한 시간이고 5분간 online + updateAt을 마지막 로그인 시간으로 가정 + 별도의 로그아웃 기능이 없으므로 현재 시간으로부터 5분이 지나면 offline 상태로 표시 + */ + public boolean checkUserConnect() { + return updatedAt.plusSeconds(300).isAfter( Instant.now()); + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/AbstractFileRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/AbstractFileRepository.java new file mode 100644 index 00000000..032a5a74 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/AbstractFileRepository.java @@ -0,0 +1,75 @@ +package com.sprint.mission.discodeit.repository; + +import java.io.*; +import java.util.*; + +public abstract class AbstractFileRepository { + protected final Map dataMap = new HashMap<>(); + private final String filePath; + + protected AbstractFileRepository(String filePath) { + this.filePath = filePath; + + File directory = new File(new File(filePath).getParent()); + // filePath는 ~~~/.discodeit/user.ser 파일 전체 경로 + // .getParent()로 상위 디렉터리 경로(~~~/.discodeit/)를 가져온다. 이 경로를 바탕으로 File 객체를 생성 + // 초기화 시점은 해당 빈이 DI되는 시점 - FileUserRepository가 빈으로 등록되고 이때 생성자가 호출되고, .discodeit 폴더를 생성 + if (!directory.exists()) { + boolean created = directory.mkdirs(); + if (!created) { // 권한 or 경로 error + throw new IllegalStateException("디렉터리 생성에 실패했습니다.: "); + } + } + + loadData(); + } + + private void loadData() { + File file = new File(filePath); + if (!file.exists()) return; + + try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { + Map loadedData = (Map) ois.readObject(); + dataMap.putAll(loadedData); + } catch (IOException | ClassNotFoundException e) { + throw new IllegalStateException("데이터 로딩에 실패: " + filePath, e); + } + } + + protected void saveData() { + try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) { + oos.writeObject(dataMap); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public T save(T entity, ID id) { + dataMap.put(id, entity); + saveData(); + return entity; + } + + public Optional findById(ID id) { + return Optional.ofNullable(dataMap.get(id)); + } + + public List findAll() { + return new ArrayList<>(dataMap.values()); + } + + public T update(T entity, ID id) { + if (!dataMap.containsKey(id)) { + throw new IllegalArgumentException("해당 ID가 존재하지 않습니다: " + id); + } + dataMap.put(id, entity); + saveData(); + return entity; + } + + public void delete(ID id) { + dataMap.remove(id); + saveData(); + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java new file mode 100644 index 00000000..553c9e5f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.entity.BinaryContent; + +import java.util.Optional; +import java.util.UUID; + +public interface BinaryContentRepository { + BinaryContent save(BinaryContent content); + Optional find(UUID id); + Optional findByUserId(UUID userId); + void delete(UUID id); +} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java index 373aa4a3..0fb70271 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java @@ -11,10 +11,10 @@ public interface ChannelRepository { Channel save(Channel channel); // 2. 채널 단건 조회 - Optional getChannelById(UUID channelId); + Optional findById(UUID channelId); // 3. 채널 전체 조회 - List getAllChannels(); + List findAll(); // 4. 채널 이름 수정 void update(Channel channel); diff --git a/src/main/java/com/sprint/mission/discodeit/repository/MessageDataStore.java b/src/main/java/com/sprint/mission/discodeit/repository/MessageDataStore.java new file mode 100644 index 00000000..9e8723c8 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/MessageDataStore.java @@ -0,0 +1,18 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.entity.Message; +import lombok.Getter; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Getter +public class MessageDataStore implements Serializable { + private static final long serialVersionUID = 1L; + private Map messageMap = new HashMap<>(); + private Map> channelMessagesMap = new HashMap<>(); + private Map> userMessagesMap = new HashMap<>(); +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java index f8a74e47..3c76183f 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java @@ -16,15 +16,12 @@ public interface MessageRepository { // 메시지를 삭제 void delete(UUID messageId); + // 여러 메시지를 한 번에 삭제 + void deleteAll(List messages); + // 특정 채널에 속한 모든 메시지를 조회 List findByChannelId(UUID channelId); // 특정 유저가 보낸 모든 메시지를 조회 List findBySenderId(UUID senderId); - - // 특정 채널에서 메시지를 제거 - void removeFromChannel(UUID channelId, Message message); - - // 특정 유저의 메시지 목록에서 메시지를 제거 - void removeFromUser(UUID userId, Message message); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java new file mode 100644 index 00000000..8d45ccb6 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java @@ -0,0 +1,21 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.entity.ReadStatus; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +public interface ReadStatusRepository { + ReadStatus save(ReadStatus readStatus); + + Optional findById(UUID id); + Optional findByUserIdAndChannelId(UUID userId, UUID channelId); + List findAllByUserId(UUID userId); + List findByChannelId(UUID channelId); + + void updateLastReadAt(UUID id, Instant newReadAt); + + void deleteById(UUID id); + void deleteAll(List readStatuses); +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java index e9b1159f..bab3ab43 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java @@ -8,18 +8,18 @@ public interface UserRepository { // 유저 생성 - void save(User user); + User save(User user); // 유저 단건 조회 (ID로) - Optional getUserById(UUID id); + Optional findById(UUID id); + Optional findByUsername(String username); // 모든 유저 조회 - List getAllUsers(); + List findAll(); // 유저 수정 - void update(User user); + User update(User user); // 유저 삭제 void delete(UUID id); - } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java new file mode 100644 index 00000000..d4387426 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java @@ -0,0 +1,14 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.entity.UserStatus; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface UserStatusRepository { + UserStatus save(UserStatus status); + Optional find(UUID id); + List findAll(); + void delete(UUID id); +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java new file mode 100644 index 00000000..ce59d683 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/file/FileBinaryContentRepository.java @@ -0,0 +1,46 @@ +package com.sprint.mission.discodeit.repository.file; + +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.repository.AbstractFileRepository; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.nio.file.Paths; +import java.util.Optional; +import java.util.UUID; + +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file") +@Repository +public class FileBinaryContentRepository extends AbstractFileRepository implements BinaryContentRepository { + + public FileBinaryContentRepository( + @Value("${discodeit.repository.file-directory}") String fileDirectory + ) { + super(Paths.get(System.getProperty("user.dir"), fileDirectory, "binaryContent.ser").toString()); + } + + @Override + public BinaryContent save(BinaryContent content) { + return super.save(content, content.getId()); + } + + @Override + public Optional find(UUID id) { + return super.findById(id); + } + + @Override + public Optional findByUserId(UUID userId) { + return findAll().stream() + .filter(content -> content.getUserId().equals(userId)) + .findFirst(); + } + + @Override + public void delete(UUID id) { + super.delete(id); + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java index e8a296da..5b29c3ab 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/file/FileChannelRepository.java @@ -1,96 +1,67 @@ package com.sprint.mission.discodeit.repository.file; import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.repository.AbstractFileRepository; import com.sprint.mission.discodeit.repository.ChannelRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; -import java.io.*; -import java.util.*; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.UUID; -public class FileChannelRepository implements ChannelRepository { - private static final String FILE_PATH = "channels.ser"; - private final Map channelData; +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file") +@Repository +public class FileChannelRepository extends AbstractFileRepository implements ChannelRepository { - public FileChannelRepository() { - this.channelData = loadChannelData(); + public FileChannelRepository( + @Value("${discodeit.repository.file-directory}") String fileDirectory + ) { + super(Paths.get(System.getProperty("user.dir"), fileDirectory, "channel.ser").toString()); } - // 직렬화된 파일에서 채널 데이터를 불러옴 - private Map loadChannelData() { - File file = new File(FILE_PATH); - if (!file.exists()) { - return new HashMap<>(); - } - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - return (Map) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - return new HashMap<>(); - } - } - - // 채널 데이터를 파일로 저장 - private void saveChannelData() { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) { - oos.writeObject(channelData); - } catch (IOException e) { - e.printStackTrace(); - } - } - - // 1. 채널 생성 @Override public Channel save(Channel channel) { - channelData.put(channel.getId(), channel); - saveChannelData(); + super.save(channel, channel.getId()); return channel; } - // 2. 채널 단건 조회 @Override - public Optional getChannelById(UUID channelId) { - return Optional.ofNullable(channelData.get(channelId)); + public Optional findById(UUID channelId) { + return super.findById(channelId); } - // 3. 채널 전체 조회 @Override - public List getAllChannels() { - return new ArrayList<>(channelData.values()); + public List findAll() { + return super.findAll(); } - // 4. 채널 수정 (채널 이름 등 변경사항 반영) @Override public void update(Channel channel) { - if (!channelData.containsKey(channel.getId())) { - throw new IllegalArgumentException("해당 채널이 존재하지 않습니다: " + channel.getId()); - } - channelData.put(channel.getId(), channel); // 기존 채널 덮어쓰기 - saveChannelData(); + super.update(channel, channel.getId()); } - // 5. 채널 삭제 @Override public void delete(UUID channelId) { - channelData.remove(channelId); - saveChannelData(); + super.delete(channelId); } - // 6. 유저가 만든 채널 전체 삭제 @Override public void deleteByOwnerId(UUID userId) { - channelData.entrySet().removeIf(entry -> entry.getValue().getChannelOwner().getId().equals(userId)); - saveChannelData(); + dataMap.entrySet().removeIf(entry -> entry.getValue().getChannelOwner().getId().equals(userId)); + saveData(); } - // 7. 유저가 참여 중인 모든 채널에서 유저 제거 @Override public void removeUserFromAllChannels(UUID userId) { - for (Channel channel : channelData.values()) { - channel.getChannelUsers().removeIf(user -> user.getId().equals(userId)); + for (Channel channel : dataMap.values()) { + channel.getChannelMembers().removeIf(user -> user.getId().equals(userId)); } - saveChannelData(); + saveData(); } - - /* + /* Repository: 데이터를 영속화(storage)하는 계층 데이터를 단순히 저장/조회/삭제/갱신 getChannelById, save, delete, update 같은 CRUD 담당 diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java index 466c52a2..4ff86ce7 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/file/FileMessageRepository.java @@ -1,40 +1,47 @@ package com.sprint.mission.discodeit.repository.file; import com.sprint.mission.discodeit.entity.Message; +import com.sprint.mission.discodeit.repository.MessageDataStore; import com.sprint.mission.discodeit.repository.MessageRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; import java.io.*; +import java.nio.file.Paths; import java.util.*; +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file") +@Repository public class FileMessageRepository implements MessageRepository { - private static final String FILE_PATH = "messages.ser"; - private Map messageMap = new HashMap<>(); - private Map> channelMessagesMap = new HashMap<>(); - private Map> userMessagesMap = new HashMap<>(); + private final String filePath; + private MessageDataStore dataStore; - public FileMessageRepository() { - loadData(); + public FileMessageRepository( + @Value("${discodeit.repository.file-directory}") String fileDirectory + ) { + this.filePath = Paths.get(System.getProperty("user.dir"), fileDirectory, "messages.ser").toString(); + loadData(); // 데이터를 로드 } private void loadData() { - File file = new File(FILE_PATH); - if (!file.exists()) return; + File file = new File(filePath); + if (!file.exists()) { + dataStore = new MessageDataStore(); // 초기화 + return; + } try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - messageMap = (Map) ois.readObject(); - channelMessagesMap = (Map>) ois.readObject(); - userMessagesMap = (Map>) ois.readObject(); - } catch (Exception e) { - e.printStackTrace(); + dataStore = (MessageDataStore) ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new IllegalStateException("Message 데이터 로딩 실패", e); } } private void saveData() { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) { - oos.writeObject(messageMap); - oos.writeObject(channelMessagesMap); - oos.writeObject(userMessagesMap); + try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) { + oos.writeObject(dataStore); } catch (IOException e) { e.printStackTrace(); } @@ -42,47 +49,62 @@ private void saveData() { @Override public Message save(Message message) { - messageMap.put(message.getId(), message); - channelMessagesMap.computeIfAbsent(message.getChannelId(), k -> new ArrayList<>()).add(message); - userMessagesMap.computeIfAbsent(message.getSenderId(), k -> new ArrayList<>()).add(message); + dataStore.getMessageMap().put(message.getId(), message); + + // 채널 인덱스 + dataStore.getChannelMessagesMap() + .computeIfAbsent(message.getChannelId(), k -> new ArrayList<>()) + .add(message); + + // 유저 인덱스 + dataStore.getUserMessagesMap() + .computeIfAbsent(message.getSenderId(), k -> new ArrayList<>()) + .add(message); + saveData(); return message; } @Override public Optional findById(UUID messageId) { - return Optional.ofNullable(messageMap.get(messageId)); + return Optional.ofNullable(dataStore.getMessageMap().get(messageId)); } @Override public void delete(UUID messageId) { - Message msg = messageMap.remove(messageId); - if (msg != null) { - channelMessagesMap.getOrDefault(msg.getChannelId(), new ArrayList<>()).remove(msg); - userMessagesMap.getOrDefault(msg.getSenderId(), new ArrayList<>()).remove(msg); + Message message = dataStore.getMessageMap().remove(messageId); + if (message != null) { + dataStore.getChannelMessagesMap() + .getOrDefault(message.getChannelId(), new ArrayList<>()) + .remove(message); + dataStore.getUserMessagesMap() + .getOrDefault(message.getSenderId(), new ArrayList<>()) + .remove(message); saveData(); } } @Override - public List findByChannelId(UUID channelId) { - return new ArrayList<>(channelMessagesMap.getOrDefault(channelId, Collections.emptyList())); + public void deleteAll(List messages) { + for (Message message : messages) { + delete(message.getId()); + } } @Override - public List findBySenderId(UUID senderId) { - return new ArrayList<>(userMessagesMap.getOrDefault(senderId, Collections.emptyList())); + public List findByChannelId(UUID channelId) { + return new ArrayList<>(dataStore.getChannelMessagesMap() + .getOrDefault(channelId, Collections.emptyList())); } @Override - public void removeFromChannel(UUID channelId, Message message) { - channelMessagesMap.getOrDefault(channelId, new ArrayList<>()).remove(message); - saveData(); + public List findBySenderId(UUID senderId) { + return new ArrayList<>(dataStore.getUserMessagesMap() + .getOrDefault(senderId, Collections.emptyList())); } - @Override - public void removeFromUser(UUID userId, Message message) { - userMessagesMap.getOrDefault(userId, new ArrayList<>()).remove(message); - saveData(); - } -} \ No newline at end of file + /* + 세 개의 Map을 각각 직렬화하는 대신, (dataStore 단일 객체만 .ser 파일로 직렬화) + 하나의 Wrapper 객체(MessageDataStore)로 묶어 통합 직렬화하면 구조 변경 시 대응이 가능하다. + */ +} diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java new file mode 100644 index 00000000..92a78740 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/file/FileReadStatusRepository.java @@ -0,0 +1,78 @@ +package com.sprint.mission.discodeit.repository.file; + +import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.repository.AbstractFileRepository; +import com.sprint.mission.discodeit.repository.ReadStatusRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.nio.file.Paths; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file") +@Repository +public class FileReadStatusRepository extends AbstractFileRepository implements ReadStatusRepository { + + public FileReadStatusRepository( + @Value("${discodeit.repository.file-directory}") String fileDirectory + ) { + super(Paths.get(System.getProperty("user.dir"), fileDirectory, "readStatus.ser").toString()); + } + + @Override + public ReadStatus save(ReadStatus readStatus) { + return super.save(readStatus, readStatus.getId()); + } + + @Override + public Optional findById(UUID id) { + return super.findById(id); + } + + @Override + public Optional findByUserIdAndChannelId(UUID userId, UUID channelId) { + return super.findAll().stream() + .filter(rs -> rs.getUserId().equals(userId) && rs.getChannelId().equals(channelId)) + .findFirst(); + } + + @Override + public List findAllByUserId(UUID userId) { + return super.findAll().stream() + .filter(rs -> rs.getUserId().equals(userId)) + .collect(Collectors.toList()); + } + + @Override + public List findByChannelId(UUID channelId) { + return super.findAll().stream() + .filter(rs -> rs.getChannelId().equals(channelId)) + .collect(Collectors.toList()); + } + + @Override + public void updateLastReadAt(UUID id, Instant newReadAt) { + ReadStatus rs = super.findById(id).orElse(null); + if (rs != null) { + rs.updateLastReadAt(newReadAt); + super.save(rs, id); + } + } + + @Override + public void deleteById(UUID id) { + super.delete(id); + } + + @Override + public void deleteAll(List readStatuses) { + for (ReadStatus rs : readStatuses) { + super.delete(rs.getId()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java index 37d455ed..da2a78bd 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserRepository.java @@ -1,75 +1,60 @@ package com.sprint.mission.discodeit.repository.file; import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.repository.AbstractFileRepository; import com.sprint.mission.discodeit.repository.UserRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; -import java.io.*; -import java.util.*; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.UUID; -public class FileUserRepository implements UserRepository { - private static final String FILE_PATH = "users.ser"; - private final Map userData; +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file") +@Repository +public class FileUserRepository extends AbstractFileRepository implements UserRepository { + //private static final String FILE_PATH = "users.ser"; - public FileUserRepository() { - this.userData = loadUserData(); + public FileUserRepository( + @Value("${discodeit.repository.file-directory}") String fileDirectory + ) { + super(Paths.get(System.getProperty("user.dir"), fileDirectory, "user.ser").toString()); } - // 파일로부터 유저 데이터 로드 - private Map loadUserData() { - File file = new File(FILE_PATH); - if (!file.exists()) { - return new HashMap<>(); - } - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - return (Map) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - return new HashMap<>(); - } - } - - // 유저 데이터를 파일로 저장 - private void saveUserData() { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) { - oos.writeObject(userData); - } catch (IOException e) { - e.printStackTrace(); - } + @Override + public User save(User user) { + super.save(user, user.getId()); + return user; } - // 유저 저장 (create) @Override - public void save(User user) { - userData.put(user.getId(), user); - saveUserData(); + public Optional findById(UUID id) { + return super.findById(id); } - // 유저 단건 조회 @Override - public Optional getUserById(UUID id) { - return Optional.ofNullable(userData.get(id)); + public Optional findByUsername(String username) { + return super.findAll().stream() + .filter(user -> user.getUsername().equals(username)) + .findFirst(); } - // 모든 유저 조회 @Override - public List getAllUsers() { - return new ArrayList<>(userData.values()); + public List findAll() { + return super.findAll(); } - // 유저 수정 @Override - public void update(User user) { - if (!userData.containsKey(user.getId())) { - throw new IllegalArgumentException("해당 ID의 유저가 존재하지 않습니다: " + user.getId()); - } - userData.put(user.getId(), user); // 업데이트는 덮어쓰기 - saveUserData(); + public User update(User user) { + super.update(user, user.getId()); + return user; } - // 유저 삭제 @Override public void delete(UUID id) { - userData.remove(id); - saveUserData(); + super.delete(id); } } + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java new file mode 100644 index 00000000..33d4fd6f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/file/FileUserStatusRepository.java @@ -0,0 +1,45 @@ +package com.sprint.mission.discodeit.repository.file; + +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.repository.AbstractFileRepository; +import com.sprint.mission.discodeit.repository.UserStatusRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "file") +@Repository +public class FileUserStatusRepository extends AbstractFileRepository implements UserStatusRepository { + + public FileUserStatusRepository( + @Value("${discodeit.repository.file-directory}") String fileDirectory + ) { + super(Paths.get(System.getProperty("user.dir"), fileDirectory, "userStatus.ser").toString()); + } + + @Override + public UserStatus save(UserStatus status) { + return super.save(status, status.getId()); + } + + @Override + public Optional find(UUID id) { + return super.findById(id); + } + + @Override + public List findAll() { + return super.findAll(); + } + + @Override + public void delete(UUID id) { + super.delete(id); + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfBinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfBinaryContentRepository.java new file mode 100644 index 00000000..39f9df74 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfBinaryContentRepository.java @@ -0,0 +1,43 @@ +package com.sprint.mission.discodeit.repository.jcf; + +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf", matchIfMissing = true) +@Repository +public class JcfBinaryContentRepository implements BinaryContentRepository { + + private final Map store = new HashMap<>(); + + @Override + public BinaryContent save(BinaryContent content) { + store.put(content.getId(), content); + return content; + } + + @Override + public Optional find(UUID id) { + return Optional.ofNullable(store.get(id)); + } + + @Override + public Optional findByUserId(UUID userId) { + return store.values().stream() + .filter(content -> content.getUserId().equals(userId)) + .findFirst(); + } + + @Override + public void delete(UUID id) { + store.remove(id); // 삭제 + } +} + + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfChannelRepository.java index 93d3a4ef..72c07a4d 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfChannelRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfChannelRepository.java @@ -2,9 +2,13 @@ import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.repository.ChannelRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; import java.util.*; +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf", matchIfMissing = true) +@Repository public class JcfChannelRepository implements ChannelRepository { private final Map channelMap = new HashMap<>(); @@ -15,12 +19,12 @@ public Channel save(Channel channel) { } @Override - public Optional getChannelById(UUID channelId) { + public Optional findById(UUID channelId) { return Optional.ofNullable(channelMap.get(channelId)); } @Override - public List getAllChannels() { + public List findAll() { return new ArrayList<>(channelMap.values()); } @@ -29,7 +33,7 @@ public void update(Channel channel) { if (!channelMap.containsKey(channel.getId())) { throw new IllegalArgumentException("채널을 찾을 수 없습니다: " + channel.getId()); } - channelMap.put(channel.getId(), channel); // 이미 존재하는 채널을 덮어씌운다 + channelMap.put(channel.getId(), channel); } @Override @@ -48,7 +52,7 @@ public void deleteByOwnerId(UUID userId) { @Override public void removeUserFromAllChannels(UUID userId) { for (Channel channel : channelMap.values()) { - channel.getChannelUsers().removeIf(user -> user.getId().equals(userId)); + channel.getChannelMembers().removeIf(user -> user.getId().equals(userId)); } } } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfMessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfMessageRepository.java index b3dc1362..64dcc047 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfMessageRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfMessageRepository.java @@ -2,9 +2,13 @@ import com.sprint.mission.discodeit.entity.Message; import com.sprint.mission.discodeit.repository.MessageRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; import java.util.*; +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf", matchIfMissing = true) +@Repository public class JcfMessageRepository implements MessageRepository { private final Map messageMap = new HashMap<>(); @@ -26,11 +30,12 @@ public Optional findById(UUID messageId) { @Override public void delete(UUID messageId) { - Message msg = messageMap.remove(messageId); - if (msg != null) { - removeFromChannel(msg.getChannelId(), msg); - removeFromUser(msg.getSenderId(), msg); - } + messageMap.remove(messageId); + } + + @Override + public void deleteAll(List messages) { + } @Override @@ -43,13 +48,13 @@ public List findBySenderId(UUID senderId) { return userMessagesMap.getOrDefault(senderId, Collections.emptyList()); } - @Override - public void removeFromChannel(UUID channelId, Message message) { - channelMessagesMap.getOrDefault(channelId, new ArrayList<>()).remove(message); - } - - @Override - public void removeFromUser(UUID userId, Message message) { - userMessagesMap.getOrDefault(userId, new ArrayList<>()).remove(message); - } +// @Override +// public void removeFromChannel(UUID channelId, Message message) { +// channelMessagesMap.getOrDefault(channelId, new ArrayList<>()).remove(message); +// } +// +// @Override +// public void removeFromUser(UUID userId, Message message) { +// userMessagesMap.getOrDefault(userId, new ArrayList<>()).remove(message); +// } } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfReadStatusRepository.java new file mode 100644 index 00000000..c1a53f14 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfReadStatusRepository.java @@ -0,0 +1,71 @@ +package com.sprint.mission.discodeit.repository.jcf; + +import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.repository.ReadStatusRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf", matchIfMissing = true) +@Repository +public class JcfReadStatusRepository implements ReadStatusRepository { + + private final Map storage = new HashMap<>(); + + @Override + public ReadStatus save(ReadStatus readStatus) { + storage.put(readStatus.getId(), readStatus); + return readStatus; + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(storage.get(id)); + } + + @Override + public Optional findByUserIdAndChannelId(UUID userId, UUID channelId) { + return storage.values().stream() + .filter(rs -> rs.getUserId().equals(userId) && rs.getChannelId().equals(channelId)) + .findFirst(); + } + + @Override + public List findAllByUserId(UUID userId) { + return storage.values().stream() + .filter(rs -> rs.getUserId().equals(userId)) + .collect(Collectors.toList()); + } + + @Override + public List findByChannelId(UUID channelId) { + return storage.values().stream() + .filter(rs -> rs.getChannelId().equals(channelId)) + .collect(Collectors.toList()); + } + + @Override + public void updateLastReadAt(UUID id, Instant newReadAt) { + ReadStatus rs = storage.get(id); + if (rs != null) { + rs.updateLastReadAt(newReadAt); + } + } + + @Override + public void deleteById(UUID id) { + storage.remove(id); + } + + @Override + public void deleteAll(List readStatuses) { + for (ReadStatus readStatus : readStatuses) { + storage.remove(readStatus.getId()); + } + } +} + + diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserRepository.java index 45ebb7b9..420e03a0 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserRepository.java @@ -2,9 +2,13 @@ import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.repository.UserRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; import java.util.*; +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf", matchIfMissing = true) +@Repository public class JcfUserRepository implements UserRepository { private final Map userMap; @@ -13,26 +17,35 @@ public JcfUserRepository() { } @Override - public void save(User user) { + public User save(User user) { userMap.put(user.getId(), user); + return user; } @Override - public Optional getUserById(UUID id) { + public Optional findById(UUID id) { return Optional.ofNullable(userMap.get(id)); } @Override - public List getAllUsers() { + public Optional findByUsername(String username) { + return this.findAll().stream() + .filter(user -> user.getUsername().equals(username)) + .findFirst(); + } + + @Override + public List findAll() { return new ArrayList<>(userMap.values()); } @Override - public void update(User user) { + public User update(User user) { if (!userMap.containsKey(user.getId())) { throw new IllegalArgumentException("해당 ID의 유저가 존재하지 않습니다: " + user.getId()); } userMap.put(user.getId(), user); + return user; } @Override diff --git a/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserStatusRepository.java new file mode 100644 index 00000000..ee9a728d --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/repository/jcf/JcfUserStatusRepository.java @@ -0,0 +1,37 @@ +package com.sprint.mission.discodeit.repository.jcf; + +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.repository.UserStatusRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; + +import java.util.*; + +@ConditionalOnProperty(name = "discodeit.repository.type", havingValue = "jcf", matchIfMissing = true) +@Repository +public class JcfUserStatusRepository implements UserStatusRepository { + + private final Map store = new HashMap<>(); + + @Override + public UserStatus save(UserStatus status) { + store.put(status.getId(), status); + return status; + } + + @Override + public Optional find(UUID id) { + return Optional.ofNullable(store.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(store.values()); + } + + @Override + public void delete(UUID id) { + store.remove(id); + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/service/AuthService.java b/src/main/java/com/sprint/mission/discodeit/service/AuthService.java new file mode 100644 index 00000000..fcec6441 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/AuthService.java @@ -0,0 +1,23 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.LoginRequest; +import com.sprint.mission.discodeit.repository.UserRepository; + +public class AuthService { + + private final UserRepository userRepository; + + public AuthService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public UserDto login(LoginRequest request) { + return userRepository.findAll().stream() + .filter(user -> user.getUsername().equals(request.username())) + .filter(user -> user.getPassword().equals(request.password())) // 단순 비교 + .findFirst() // 조건 만족하는 첫 번째 유저 + .map(user -> new UserDto(user, false, false)) // UserDto로 변환하여 비밀번호 정보 제외 + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 username & password 입니다.")); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java b/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java new file mode 100644 index 00000000..8e27b545 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java @@ -0,0 +1,16 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface BinaryContentService { + BinaryContent create(BinaryContentCreateRequest request, UUID userId, UUID messageId); + Optional findById(UUID id); + List findAllByIdIn(List ids); + void delete(UUID id); +} + diff --git a/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java index 15cb7231..822239e3 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java @@ -1,5 +1,9 @@ package com.sprint.mission.discodeit.service; +import com.sprint.mission.discodeit.dto.data.ChannelDto; +import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.User; @@ -11,22 +15,27 @@ public interface ChannelService { // 채널 생성, 조회(단건 , 다건), 수정, 삭제 + 멤버 추가/ 삭제 // 1. 채널 생성 - Channel createChannel(String channelName, User ownerUser); + //Channel createChannel(String channelName, User ownerUser); + Channel create(PublicChannelCreateRequest request); + Channel create(PrivateChannelCreateRequest request); // 2. 단일 채널 조회 - Optional getChannelById(UUID channelId); + //Optional getChannelById(UUID channelId); + Optional find(UUID channelId); // 3. 전체 채널 조회 - List getAllChannels(); + //List getAllChannels(); + List findAllByUserId(UUID userId); // 4. 채널 이름 수정 - void updateChannelName(UUID channelId, String channelName); + //void updateChannelName(UUID channelId, String channelName); + Channel update(UUID channelId, PublicChannelUpdateRequest request); // 채널 소유자 변경 ... 채널 소유자가 탈퇴한다면? 채널도 삭제된다. <- 이 전제로 1) 필요없는 메서드로 진행 // 2) 채널 소유자가 떠나도 채널이 유지되어야 하는 경우에는 해당 메서드가 필요 //void updateChannelOwner(UUID channelId, UUID ownerUserId); - // 5. 채널 삭제(채널 소유자만 삭제 가능) + // 5. 채널 삭제 void deleteChannel(UUID channelId); // 6. 멤버 추가 @@ -38,9 +47,11 @@ public interface ChannelService { // 8. 채널에 속한 멤버 조회 List getChannelMembers(UUID channelId); + /* UserService에서 channelService가 아닌 channelRepository를 의존(참조)하므로 더이상 쓰이지 않음 // 9. 유저가 만든 모든 채널 삭제 void deleteChannelsCreatedByUser(UUID userId); // 10. 유저가 참여 중인 모든 채널에서 탈퇴 void removeUserFromAllChannels(UUID userId); + */ } diff --git a/src/main/java/com/sprint/mission/discodeit/service/MessageService.java b/src/main/java/com/sprint/mission/discodeit/service/MessageService.java index 1193471d..1b06c996 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/MessageService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/MessageService.java @@ -1,14 +1,19 @@ package com.sprint.mission.discodeit.service; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; +import com.sprint.mission.discodeit.dto.request.MessageUpdateRequest; import com.sprint.mission.discodeit.entity.Message; import java.util.List; +import java.util.Optional; import java.util.UUID; public interface MessageService { // 메시지 생성: 채널, 발신자, 내용 필요 - Message createMessage(UUID channelId, UUID senderId, String content); + //Message createMessage(UUID channelId, UUID senderId, String content); + Message createMessage(MessageCreateRequest messageCreateRequest, List binaryContentCreateRequests); // 특정 채널에서 내가 보낸 메시지들 조회 List getMessagesBySenderInChannel(UUID channelId, UUID senderId); @@ -23,11 +28,17 @@ public interface MessageService { List getAllReceivedMessages(UUID receiverId); // 메시지 수정 (내가 보낸 메시지만 가능) - void updateMessage(UUID messageId, UUID senderId, String newContent); + Message updateMessage(UUID messageId, MessageUpdateRequest request); // 메시지 삭제 (내가 보낸 메시지만 가능) void deleteMessage(UUID messageId, UUID senderId); + // 해당 채널 전체 메시지를 요청자가 조회 - List getAllMessagesInChannel(UUID channelId, UUID requesterId); + //List getAllMessagesInChannel(UUID channelId, UUID requesterId); + // 특정 Channel의 Message 목록을 조회 + List findAllByChannelId(UUID channelId); + + Optional getMessageById(UUID messageId); + String formatMessage(Message message); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java new file mode 100644 index 00000000..b8b07dc3 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.request.ReadStatusCreateRequest; +import com.sprint.mission.discodeit.dto.request.ReadStatusUpdateRequest; +import com.sprint.mission.discodeit.entity.ReadStatus; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ReadStatusService { + ReadStatus create(ReadStatusCreateRequest request); + Optional findById(UUID id); + List findAllByUserId(UUID userId); + ReadStatus update(UUID id, ReadStatusUpdateRequest request); + void delete(UUID id); +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserService.java b/src/main/java/com/sprint/mission/discodeit/service/UserService.java index d73980fe..c23ab597 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/UserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/UserService.java @@ -1,5 +1,9 @@ package com.sprint.mission.discodeit.service; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserUpdateRequest; import com.sprint.mission.discodeit.entity.User; import java.util.List; @@ -8,14 +12,21 @@ public interface UserService { // 1. 유저 생성 - User createUser(String username, String email); + User createUser(UserCreateRequest userCreateRequest, Optional profileCreateRequest); + // 2. 유저 단건 조회 - Optional getUserById(UUID id); + //Optional getUserById(UUID id); + Optional find(UUID userId); + // 3. 유저 전체 조회 - List getAllUsers(); + //List getAllUsers(); + List findAll(); + // 4. 유저 수정 - void updateUserName(UUID id,String name); - void updateUserEmail(UUID id,String email); + //void updateUserName(UUID id,String name); + //void updateUserEmail(UUID id,String email); + User update(UUID userId, UserUpdateRequest userUpdateRequest, Optional profileCreateRequest); + // 5. 유저 삭제 - void deleteUser(UUID id); + void delete(UUID userId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java new file mode 100644 index 00000000..66fdea0f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.request.UserStatusCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserStatusUpdateRequest; +import com.sprint.mission.discodeit.entity.UserStatus; + +import java.util.List; +import java.util.UUID; + +public interface UserStatusService { + UserStatus create(UserStatusCreateRequest request); + UserStatus findById(UUID id); + UserStatus update(UserStatusUpdateRequest request); + UserStatus updateByUserId(UUID userId); + void delete(UUID userId); +} + diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java new file mode 100644 index 00000000..e6aee9b2 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java @@ -0,0 +1,56 @@ +package com.sprint.mission.discodeit.service.basic; + +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import com.sprint.mission.discodeit.service.BinaryContentService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class BasicBinaryContentService implements BinaryContentService { + + private final BinaryContentRepository binaryContentRepository; + + public BasicBinaryContentService(BinaryContentRepository binaryContentRepository) { + this.binaryContentRepository = binaryContentRepository; + } + + @Override + public BinaryContent create(BinaryContentCreateRequest request, UUID userId, UUID messageId) { + if (!request.isValid()) { + throw new IllegalArgumentException("유효하지 않은 파일 정보입니다."); + } + + BinaryContent content = (messageId != null) + ? new BinaryContent(request.fileName(), userId, messageId) // 메시지 첨부파일 + : new BinaryContent(request.fileName(), userId); // 사용자 프로필 이미지 + + return binaryContentRepository.save(content); + } + + @Override + public Optional findById(UUID id) { + return binaryContentRepository.find(id); + } + + @Override + public List findAllByIdIn(List ids) { // id목록으로 조회 List ids = List.of(id1, id2); + if (ids == null || ids.isEmpty()) { + throw new IllegalArgumentException("ID 리스트가 비어있거나 null입니다."); // 조회 결과 없음 -> 빈 리스트를 리턴 or exception throw + } + + return ids.stream() + .map(binaryContentRepository::find) // 각 Id에 대해 Optional 반환 + .flatMap(Optional::stream) // 존재하는 것만 뽑아낸다. + .toList(); // List<>로 + } + + @Override + public void delete(UUID id) { + binaryContentRepository.delete(id); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java index 59cef58a..24ae3a87 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java @@ -1,64 +1,179 @@ package com.sprint.mission.discodeit.service.basic; -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.dto.data.ChannelDto; +import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.entity.*; import com.sprint.mission.discodeit.repository.ChannelRepository; +import com.sprint.mission.discodeit.repository.MessageRepository; +import com.sprint.mission.discodeit.repository.ReadStatusRepository; +import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; +@Service +@RequiredArgsConstructor public class BasicChannelService implements ChannelService { private final ChannelRepository channelRepository; - private final UserService userService; + private final UserRepository userRepository; + private final MessageRepository messageRepository; + private final ReadStatusRepository readStatusRepository; - public BasicChannelService(ChannelRepository channelRepository, UserService userService) { - this.channelRepository = channelRepository; - this.userService = userService; + @Override + public Channel create(PublicChannelCreateRequest request) { + User ownerUser = userRepository.findById(request.ownerUserId()) + .orElseThrow(() -> new IllegalArgumentException("유효한 사용자 ID가 아닙니다.")); + + boolean isDuplicate = channelRepository.findAll().stream() + .anyMatch(c -> c.getChannelOwner().getId().equals(ownerUser.getId()) + && c.getChannelName().equals(request.channelName())); + + if (isDuplicate) { + throw new IllegalArgumentException("이미 같은 이름의 채널이 존재합니다."); + } + + List members = new ArrayList<>(); + members.add(ownerUser); + + Channel channel = new Channel(request.channelName(), ownerUser, members, ChannelType.PUBLIC); + return channelRepository.save(channel); } @Override - public Channel createChannel(String channelName, User ownerUser) { - boolean isDuplicate = channelRepository.getAllChannels().stream() + public Channel create(PrivateChannelCreateRequest request) { + User ownerUser = userRepository.findById(request.ownerUserId()) + .orElseThrow(() -> new IllegalArgumentException("유효한 사용자 ID가 아닙니다.")); + + // 채널 이름 중복 확인 + boolean isDuplicate = channelRepository.findAll().stream() .anyMatch(c -> c.getChannelOwner().getId().equals(ownerUser.getId()) - && c.getChannelName().equals(channelName)); + && c.getChannelName().equals(request.channelName())); if (isDuplicate) { throw new IllegalArgumentException("이미 같은 이름의 채널이 존재합니다."); } + // 채널 생성 시 참여자와 ReadStatus 처리 List members = new ArrayList<>(); members.add(ownerUser); - Channel channel = new Channel(channelName, ownerUser, members); + Channel channel = new Channel(request.channelName(), ownerUser, members, ChannelType.PRIVATE); + // ReadStatus 생성 (참여자들에 대해 ReadStatus 설정) + for (UUID userId : request.memberIds()) { + User member = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("유효한 사용자 ID가 아닙니다.")); + members.add(member); + // ReadStatus 생성 로직 추가 + ReadStatus readStatus = new ReadStatus(member.getId(), channel.getId()); + readStatusRepository.save(readStatus); + } + return channelRepository.save(channel); } + @Override - public Optional getChannelById(UUID channelId) { - return channelRepository.getChannelById(channelId); + public Optional find(UUID channelId) { + Channel channel = channelRepository.findById(channelId) + .orElseThrow(() -> new IllegalArgumentException("채널이 존재하지 않습니다.")); + + // 가장 최근 메시지 시간 조회 + List messages = messageRepository.findByChannelId(channelId); + + LocalDateTime lastMessageTime = messages.stream() + .map(Message::getCreatedAt) + .map(i -> LocalDateTime.ofInstant(i, ZoneId.systemDefault())) + .max(LocalDateTime::compareTo) + .orElse(null); + + // PRIVATE 채널이면 멤버 ID 리스트 포함 + List memberIds = null; + if (channel.getChannelType() == ChannelType.PRIVATE) { + memberIds = channel.getChannelMembers().stream() + .map(User::getId) + .collect(Collectors.toList()); + } + + ChannelDto channelDto = new ChannelDto( + channel.getId(), + channel.getChannelName(), + channel.getChannelOwner().getId(), + channel.getChannelType(), + memberIds, + lastMessageTime + ); + + return Optional.of(channelDto); } @Override - public List getAllChannels() { - return channelRepository.getAllChannels(); + public List findAllByUserId(UUID userId) { + // 1. PUBLIC 채널은 전체 조회 + // 2. PRIVATE 채널은 userId가 참여한 채널만 조회 + List channels = channelRepository.findAll(); + + return channels.stream() + .filter(channel -> { + if (channel.getChannelType() == ChannelType.PUBLIC) { + return true; // PUBLIC 채널은 모두 조회 + } else if (channel.getChannelType() == ChannelType.PRIVATE) { + // PRIVATE 채널인 경우 userId가 참여한 채널만 조회 + return channel.getChannelMembers().stream() + .anyMatch(member -> member.getId().equals(userId)); + } + return false; + }) + .map(channel -> { + List messages = messageRepository.findByChannelId(channel.getId()); + LocalDateTime lastMessageTime = messages.stream() + .map(Message::getCreatedAt) + .map(i -> LocalDateTime.ofInstant(i, ZoneId.systemDefault())) + .max(LocalDateTime::compareTo) + .orElse(null); + + List memberIds = null; + if (channel.getChannelType() == ChannelType.PRIVATE) { + memberIds = channel.getChannelMembers().stream() + .map(User::getId) + .collect(Collectors.toList()); + } + + return new ChannelDto( + channel.getId(), + channel.getChannelName(), + channel.getChannelOwner().getId(), + channel.getChannelType(), + memberIds, + lastMessageTime + ); + }) + .collect(Collectors.toList()); } @Override - public void updateChannelName(UUID channelId, String newChannelName) { - Channel channel = channelRepository.getChannelById(channelId) + public Channel update(UUID channelId, PublicChannelUpdateRequest request){ + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널입니다.")); + String newChannelName = request.channelName(); + if (channel.getChannelName().equals(newChannelName)) { throw new IllegalArgumentException("채널 이름이 기존과 동일합니다."); } UUID ownerId = channel.getChannelOwner().getId(); - boolean isDuplicate = channelRepository.getAllChannels().stream() + boolean isDuplicate = channelRepository.findAll().stream() .anyMatch(c -> c.getChannelOwner().getId().equals(ownerId) && c.getChannelName().equals(newChannelName)); @@ -68,18 +183,33 @@ public void updateChannelName(UUID channelId, String newChannelName) { channel.updateChannelName(newChannelName); channelRepository.update(channel); + return channel; } @Override public void deleteChannel(UUID channelId) { + // 1. 채널 존재 여부 검증 + Channel channel = channelRepository.findById(channelId) + .orElseThrow(() -> new IllegalArgumentException("채널이 존재하지 않습니다.")); + + // 2. 관련된 메시지 삭제 + List messages = messageRepository.findByChannelId(channelId); + messageRepository.deleteAll(messages); + + // 3. 관련된 읽음 상태(읽은 메시지) 삭제 + List readStatuses = readStatusRepository.findByChannelId(channelId); + readStatusRepository.deleteAll(readStatuses); + + // 4. 채널 삭제 channelRepository.delete(channelId); } + @Override public void addMember(UUID channelId, UUID userId) { - Channel channel = channelRepository.getChannelById(channelId) + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - User user = userService.getUserById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다.")); channel.addChannelUser(user); @@ -88,9 +218,9 @@ public void addMember(UUID channelId, UUID userId) { @Override public void removeMember(UUID channelId, UUID userId) { - Channel channel = channelRepository.getChannelById(channelId) + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - User user = userService.getUserById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다.")); channel.removeChannelUser(user); @@ -99,18 +229,8 @@ public void removeMember(UUID channelId, UUID userId) { @Override public List getChannelMembers(UUID channelId) { - Channel channel = channelRepository.getChannelById(channelId) + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - return new ArrayList<>(channel.getChannelUsers()); - } - - @Override - public void deleteChannelsCreatedByUser(UUID userId) { - channelRepository.deleteByOwnerId(userId); - } - - @Override - public void removeUserFromAllChannels(UUID userId) { - channelRepository.removeUserFromAllChannels(userId); + return new ArrayList<>(channel.getChannelMembers()); } -} +} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java index c2d38729..6ec51c5d 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java @@ -1,53 +1,84 @@ package com.sprint.mission.discodeit.service.basic; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; +import com.sprint.mission.discodeit.dto.request.MessageUpdateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.Message; -import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import com.sprint.mission.discodeit.repository.ChannelRepository; import com.sprint.mission.discodeit.repository.MessageRepository; -import com.sprint.mission.discodeit.service.ChannelService; import com.sprint.mission.discodeit.service.MessageService; import com.sprint.mission.discodeit.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.*; +@Service +@RequiredArgsConstructor public class BasicMessageService implements MessageService { private final MessageRepository messageRepository; private final UserService userService; - private final ChannelService channelService; + private final ChannelRepository channelRepository; + private final BinaryContentRepository binaryContentRepository; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM월 dd일 a hh시 mm분 ss초"); - public BasicMessageService(MessageRepository messageRepository, UserService userService, ChannelService channelService) { - this.messageRepository = messageRepository; - this.userService = userService; - this.channelService = channelService; - } - @Override - public Message createMessage(UUID channelId, UUID senderId, String content) { - Channel channel = channelService.getChannelById(channelId) + public Message createMessage(MessageCreateRequest messageCreateRequest, List binaryContentCreateRequests) { + UUID channelId = messageCreateRequest.channelId(); + UUID senderId = messageCreateRequest.senderId(); + + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - User sender = userService.getUserById(senderId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다.")); + UserDto senderDTO = userService.find(senderId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다.")); - if (channel.getChannelUsers().size() < 2) { + if (channel.getChannelMembers().size() < 2) { throw new IllegalStateException("채널에 최소 두 명 이상의 유저가 있어야 메시지를 보낼 수 있습니다."); } - if (!channel.getChannelUsers().contains(sender)) { + boolean isMember = channel.getChannelMembers().stream() + .anyMatch(member -> member.getId().equals(senderId)); + if (!isMember) { throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); } - Message message = new Message(content, channelId, senderId); + Message message = new Message(messageCreateRequest.content(), channelId, senderId); + + if (binaryContentCreateRequests != null) { + for (BinaryContentCreateRequest fileRequest : binaryContentCreateRequests) { + if (fileRequest.isValid()) { + BinaryContent binaryContent = new BinaryContent(fileRequest.fileName(), senderId); + message.addAttachment(binaryContent.getId()); + binaryContentRepository.save(binaryContent); + } else { + throw new IllegalArgumentException("메세지에 첨부파일을 추가할 수 없습니다. 파일을 확인해주세요."); + } + } + } + return messageRepository.save(message); } + @Override + public List findAllByChannelId(UUID channelId) { + Channel channel = channelRepository.findById(channelId) + .orElseThrow(() -> new IllegalArgumentException("채널이 존재하지 않습니다.")); + + return messageRepository.findByChannelId(channelId); + } + @Override public List getMessagesBySenderInChannel(UUID channelId, UUID senderId) { - Channel channel = channelService.getChannelById(channelId) + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(senderId))) { + if (channel.getChannelMembers().stream().noneMatch(u -> u.getId().equals(senderId))) { throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); } @@ -58,9 +89,9 @@ public List getMessagesBySenderInChannel(UUID channelId, UUID senderId) @Override public List getMessagesByReceiverInChannel(UUID channelId, UUID receiverId) { - Channel channel = channelService.getChannelById(channelId) + Channel channel = channelRepository.findById(channelId) .orElseThrow(() -> new IllegalArgumentException("채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(receiverId))) { + if (channel.getChannelMembers().stream().noneMatch(u -> u.getId().equals(receiverId))) { throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); } @@ -76,8 +107,8 @@ public List getAllSentMessages(UUID senderId) { @Override public List getAllReceivedMessages(UUID receiverId) { - List myChannels = channelService.getAllChannels().stream() - .filter(ch -> ch.getChannelUsers().stream().anyMatch(u -> u.getId().equals(receiverId))) + List myChannels = channelRepository.findAll().stream() + .filter(ch -> ch.getChannelMembers().stream().anyMatch(u -> u.getId().equals(receiverId))) .map(Channel::getId) .toList(); @@ -94,15 +125,17 @@ public List getAllReceivedMessages(UUID receiverId) { } @Override - public void updateMessage(UUID messageId, UUID senderId, String newContent) { + public Message updateMessage(UUID messageId, MessageUpdateRequest request) { Message msg = messageRepository.findById(messageId) .orElseThrow(() -> new IllegalArgumentException("메시지를 찾을 수 없습니다.")); - if (!msg.getSenderId().equals(senderId)) { + + if (!msg.getSenderId().equals(request.senderId())) { throw new SecurityException("해당 메시지를 수정할 권한이 없습니다."); } - msg.updateContent(newContent); + msg.updateContent(request.newContent()); messageRepository.save(msg); + return msg; } @Override @@ -113,36 +146,30 @@ public void deleteMessage(UUID messageId, UUID senderId) { throw new SecurityException("해당 메시지를 삭제할 권한이 없습니다."); } - messageRepository.delete(messageId); - messageRepository.removeFromChannel(msg.getChannelId(), msg); - messageRepository.removeFromUser(senderId, msg); - } - - @Override - public List getAllMessagesInChannel(UUID channelId, UUID requesterId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(requesterId))) { - throw new SecurityException("채널에 접근할 수 있는 권한이 없습니다."); + // BinaryContent들도 삭제 + for (UUID attachmentId : msg.getAttachmentIds()) { + binaryContentRepository.delete(attachmentId); } - return messageRepository.findByChannelId(channelId); + messageRepository.delete(messageId); } + public Optional getMessageById(UUID messageId) { return messageRepository.findById(messageId); } public String formatMessage(Message message) { boolean isEdited = !message.getCreatedAt().equals(message.getUpdatedAt()); - long timestamp = isEdited ? message.getUpdatedAt() : message.getCreatedAt(); - String formattedDate = DATE_FORMAT.format(new Date(timestamp)); + Instant timestamp = isEdited ? message.getUpdatedAt() : message.getCreatedAt(); + + String formattedDate = DATE_FORMAT.format(Date.from(timestamp)); - Optional senderOpt = userService.getUserById(message.getSenderId()); - String senderName = senderOpt.map(User::getUsername).orElse("알 수 없음"); - String senderEmail = senderOpt.map(User::getEmail).orElse("이메일 없음"); + Optional senderOpt = userService.find(message.getSenderId()); + String senderName = senderOpt.map(UserDto::getUsername).orElse("알 수 없음"); + String senderEmail = senderOpt.map(UserDto::getEmail).orElse("이메일 없음"); - String channelName = channelService.getChannelById(message.getChannelId()) + String channelName = channelRepository.findById(message.getChannelId()) .map(Channel::getChannelName) .orElse("알 수 없음"); diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java new file mode 100644 index 00000000..3c09e2a3 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java @@ -0,0 +1,81 @@ +package com.sprint.mission.discodeit.service.basic; + +import com.sprint.mission.discodeit.dto.request.ReadStatusCreateRequest; +import com.sprint.mission.discodeit.dto.request.ReadStatusUpdateRequest; +import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.repository.ChannelRepository; +import com.sprint.mission.discodeit.repository.ReadStatusRepository; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.service.ReadStatusService; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +@Service +public class BasicReadStatusService implements ReadStatusService { + + private final ReadStatusRepository readStatusRepository; + private final UserRepository userRepository; + private final ChannelRepository channelRepository; + + public BasicReadStatusService(ReadStatusRepository readStatusRepository, UserRepository userRepository, ChannelRepository channelRepository) { + this.readStatusRepository = readStatusRepository; + this.userRepository = userRepository; + this.channelRepository = channelRepository; + } + + @Override + public ReadStatus create(ReadStatusCreateRequest request) { + if (userRepository.findById(request.userId()).isEmpty()) { + throw new IllegalArgumentException("해당 유저가 존재하지 않습니다."); + } + + if (channelRepository.findById(request.channelId()).isEmpty()) { + throw new IllegalArgumentException("해당 채널이 존재하지 않습니다."); + } + + Optional existing = readStatusRepository + .findByUserIdAndChannelId(request.userId(), request.channelId()); + + if (existing.isPresent()) { + throw new IllegalArgumentException("해당 유저와 채널의 ReadStatus는 이미 존재합니다."); + } + + ReadStatus newStatus = new ReadStatus(request.userId(), request.channelId()); + + return readStatusRepository.save(newStatus); + } + + @Override + public Optional findById(UUID id) { + return readStatusRepository.findById(id); + } + + @Override + public List findAllByUserId(UUID userId) { + return readStatusRepository.findAllByUserId(userId); + } + + @Override + public ReadStatus update(UUID id, ReadStatusUpdateRequest request) { + Instant newLastReadAt = request.newLastReadAt(); + ReadStatus readStatus = readStatusRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("해당 ReadStatus가 존재하지 않습니다.")); + + readStatus.updateLastReadAt(newLastReadAt); + return readStatusRepository.save(readStatus); + } + + + @Override + public void delete(UUID id) { + if (readStatusRepository.findById(id).isEmpty()) { + throw new IllegalArgumentException("삭제할 ReadStatus가 존재하지 않습니다."); + } + readStatusRepository.deleteById(id); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java index 63d45dc4..ed750882 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java @@ -1,88 +1,150 @@ package com.sprint.mission.discodeit.service.basic; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserUpdateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import com.sprint.mission.discodeit.repository.ChannelRepository; +import com.sprint.mission.discodeit.repository.UserStatusRepository; import com.sprint.mission.discodeit.service.ChannelService; import com.sprint.mission.discodeit.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; +@Service +@RequiredArgsConstructor public class BasicUserService implements UserService { - private final UserRepository userRepository; - private ChannelService channelService; - + private final com.sprint.mission.discodeit.repository.UserRepository userRepository; + private final UserStatusRepository userStatusRepository; + private final BinaryContentRepository binaryContentRepository; + private final ChannelRepository channelRepository; // 생성자를 통해 Repository 주입 - public BasicUserService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - // 순환 참조 방지를 위한 setter 주입 - public void setChannelService(ChannelService channelService) { - this.channelService = channelService; - } + // @RequiredArgsConstructor 어노테이션이 자동으로 생성 @Override - public User createUser(String username, String email) { + public User createUser(UserCreateRequest userRequest, Optional profileRequest) { + String username = userRequest.username(); + String email = userRequest.email(); + if (isEmailDuplicate(email)) { throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + email); } - User user = new User(username, email); + + if (isNameDuplicate(username)) { + throw new IllegalArgumentException("이미 존재하는 이름입니다: " + username); + } + + User user = new User(username, email, userRequest.password()); userRepository.save(user); + + // UserStatus 생성 + UserStatus status = new UserStatus(user.getId()); + userStatusRepository.save(status); + + // Optional 프로필 이미지 처리 + profileRequest + .filter(BinaryContentCreateRequest::isValid) + .ifPresent(profile -> { + BinaryContent binaryContent = new BinaryContent(profile.fileName(), user.getId()); + binaryContentRepository.save(binaryContent); + }); + return user; } - @Override - public Optional getUserById(UUID id) { - return userRepository.getUserById(id); - } @Override - public List getAllUsers() { - return userRepository.getAllUsers(); + public Optional find(UUID id) { + return userRepository.findById(id) + .map(user -> { + boolean hasProfileImage = binaryContentRepository.findByUserId(user.getId()).isPresent(); + boolean isOnline = userStatusRepository.find(user.getId()) + .map(UserStatus::checkUserConnect) + .orElse(false); + return new UserDto(user, hasProfileImage, isOnline); + }); } - public void updateUserName(UUID id, String name) { - User user = userRepository.getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - user.updateName(name); - userRepository.update(user); + @Override + public List findAll() { + return userRepository.findAll().stream() + .map(user -> { + boolean hasProfileImage = binaryContentRepository.findByUserId(user.getId()).isPresent(); + boolean isOnline = userStatusRepository.find(user.getId()) + .map(UserStatus::checkUserConnect) + .orElse(false); + return new UserDto(user, hasProfileImage, isOnline); + }) + .collect(Collectors.toList()); } - public void updateUserEmail(UUID id, String email) { - User user = userRepository.getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - if (user.getEmail().equalsIgnoreCase(email)) { - throw new IllegalArgumentException("기존과 동일한 이메일입니다."); + @Override + public User update(UUID userId, UserUpdateRequest userUpdateRequest, Optional profileCreateRequest) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + userId)); + + if (userUpdateRequest.username() != null && !userUpdateRequest.username().equals(user.getUsername())) { + if (isNameDuplicate(userUpdateRequest.username())) { + throw new IllegalArgumentException("이미 존재하는 이름입니다: " + userUpdateRequest.username()); + } + user.updateName(userUpdateRequest.username()); // 이름 수정 메서드 호출 } - if (isEmailDuplicate(email)) { - throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + email); + if (userUpdateRequest.email() != null && !userUpdateRequest.email().equals(user.getEmail())) { + if (isEmailDuplicate(userUpdateRequest.email())) { + throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + userUpdateRequest.email()); + } + user.updateEmail(userUpdateRequest.email()); // 이메일 수정 메서드 호출 } - user.updateEmail(email); + profileCreateRequest + .filter(BinaryContentCreateRequest::isValid) + .ifPresent(profile -> { + BinaryContent binaryContent = new BinaryContent(profile.fileName(), user.getId()); + binaryContentRepository.save(binaryContent); + }); + userRepository.update(user); + return user; } + + @Override - public void deleteUser(UUID id) { - User user = userRepository.getUserById(id) + public void delete(UUID id) { + User user = userRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - if (channelService != null) { - channelService.deleteChannelsCreatedByUser(id); - channelService.removeUserFromAllChannels(id); - } + // 1. 유저가 만든 채널 삭제 및 참여 채널에서 제거 + channelRepository.deleteByOwnerId(id); // 유저가 만든 채널을 삭제 + channelRepository.removeUserFromAllChannels(id); // 유저가 참여한 채널에서 유저를 제거 + + // 2. 프로필 이미지 및 유저 상태 정보 삭제 + binaryContentRepository.delete(id); + userStatusRepository.delete(id); userRepository.delete(id); } private boolean isEmailDuplicate(String email) { - return userRepository.getAllUsers().stream() + return userRepository.findAll().stream() .anyMatch(user -> user.getEmail().equalsIgnoreCase(email)); } + + private boolean isNameDuplicate(String username) { + return userRepository.findAll().stream() + .anyMatch(user -> user.getUsername().equalsIgnoreCase(username)); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java new file mode 100644 index 00000000..360432ba --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java @@ -0,0 +1,73 @@ +package com.sprint.mission.discodeit.service.basic; + +import com.sprint.mission.discodeit.dto.request.UserStatusCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserStatusUpdateRequest; +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.repository.UserStatusRepository; +import com.sprint.mission.discodeit.service.UserStatusService; +import org.springframework.stereotype.Service; + +import java.util.NoSuchElementException; +import java.util.UUID; + +@Service +public class BasicUserStatusService implements UserStatusService { + + private final UserRepository userRepository; + private final UserStatusRepository userStatusRepository; + + public BasicUserStatusService(UserRepository userRepository, UserStatusRepository userStatusRepository) { + this.userRepository = userRepository; + this.userStatusRepository = userStatusRepository; + } + + @Override + public UserStatus create(UserStatusCreateRequest request) { + UUID userId = request.id(); + + if (userRepository.findById(userId).isEmpty()) { + throw new IllegalArgumentException("userStatus가 존재하지 않음"); + } + + if (userStatusRepository.find(userId).isPresent()) { + throw new IllegalStateException("userStatus가 존재하지 않음"); + } + + UserStatus status = new UserStatus(userId); + return userStatusRepository.save(status); + } + + @Override + public UserStatus findById(UUID id) { + return userStatusRepository.find(id) + .orElseThrow(() -> new NoSuchElementException("userStatus가 존재하지 않음")); + } + + @Override + public UserStatus update(UserStatusUpdateRequest request) { + UserStatus status = findById(request.id()); + status.updateTimestamp(); + return userStatusRepository.save(status); + } + + @Override + public UserStatus updateByUserId(UUID userId) { + UserStatus status = userStatusRepository.findAll().stream() + .filter(s -> s.getUserId().equals(userId)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("userStatus가 존재하지 않음")); + + status.updateTimestamp(); + return userStatusRepository.save(status); + } + + @Override + public void delete(UUID id) { + if (userStatusRepository.find(id).isEmpty()) { + throw new NoSuchElementException("userStatus가 존재하지 않음"); + } + userStatusRepository.delete(id); + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/service/factory/ServiceFactory.java b/src/main/java/com/sprint/mission/discodeit/service/factory/ServiceFactory.java deleted file mode 100644 index a0655f99..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/factory/ServiceFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.sprint.mission.discodeit.service.factory; - -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; -import com.sprint.mission.discodeit.service.file.FileChannelService; -import com.sprint.mission.discodeit.service.file.FileUserService; -import com.sprint.mission.discodeit.service.jcf.JcfChannelService; -import com.sprint.mission.discodeit.service.jcf.JcfUserService; - -public class ServiceFactory { - // 의존성 주입을 수동으로 관리하는 DI 컨테이너 - // ServiceFactory를 쓰는이유: JcfUserService와 JcfChannelService의 순환 참조 (A->B->A) 문제를 해결하고자 - // userService와 channelService를 프로그램 실행 하는 동안 하나의 인스턴스만 유지 - 싱글톤패턴 - // 어디서든 ServiceFactory.getUserService()로 같은 인스턴스를 사용할 수 있다. - private static UserService userService; - private static ChannelService channelService; - - public static void initializeServices() { - FileUserService user = new FileUserService(); // 먼저 Jcf(File)UserService를 비워서 생성한다. - channelService = new FileChannelService(user); // Jcf(File)ChannelService에 유저서비스를 넣는다. - user.setChannelService(channelService); // 생성된 channelService를 setter로 주입한다. - userService = user; - } - - public static UserService getUserService() { - return userService; - } - - public static ChannelService getChannelService() { - return channelService; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/service/file/FileChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/file/FileChannelService.java deleted file mode 100644 index 503bda35..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/file/FileChannelService.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.sprint.mission.discodeit.service.file; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; - -import java.io.*; -import java.util.*; - -public class FileChannelService implements ChannelService { - private static final String FILE_PATH = "channels.ser"; - private final Map channelData; - private final UserService userService; - - public FileChannelService(UserService userService) { - this.userService = userService; - this.channelData = loadChannelData(); - } - - private void saveChannelData() { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) { - oos.writeObject(channelData); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private Map loadChannelData() { - File file = new File(FILE_PATH); - if (!file.exists()) { - return new HashMap<>(); - } - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - return (Map) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - return new HashMap<>(); - } - } - - @Override - public Channel createChannel(String channelName, User ownerUser) { - boolean isDuplicate = channelData.values().stream() - .anyMatch(c -> c.getChannelOwner().getId().equals(ownerUser.getId()) - && c.getChannelName().equals(channelName)); - - if (isDuplicate) { - throw new IllegalArgumentException("이미 같은 이름의 채널이 존재합니다. 다른 이름을 설정해주세요."); - } - - List members = new ArrayList<>(); - members.add(ownerUser); - Channel channel = new Channel(channelName, ownerUser, members); - channelData.put(channel.getId(), channel); - saveChannelData(); - return channel; - } - - @Override - public Optional getChannelById(UUID channelId) { - return Optional.ofNullable(channelData.get(channelId)); - } - - @Override - public List getAllChannels() { - return new ArrayList<>(channelData.values()); - } - - @Override - public void updateChannelName(UUID channelId, String newChannelName) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - - if (channel.getChannelName().equals(newChannelName)) { - throw new IllegalArgumentException("채널 이름이 기존과 동일합니다."); - } - - UUID ownerId = channel.getChannelOwner().getId(); - boolean isDuplicate = channelData.values().stream() - .anyMatch(c -> c.getChannelOwner().getId().equals(ownerId) - && c.getChannelName().equals(newChannelName)); - - if (isDuplicate) { - throw new IllegalArgumentException("해당 채널명은 이미 사용 중입니다."); - } - - channel.updateChannelName(newChannelName); - saveChannelData(); - } - - @Override - public void deleteChannel(UUID channelId) { - channelData.remove(channelId); - saveChannelData(); - } - - @Override - public void addMember(UUID channelId, UUID userId) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - User user = userService.getUserById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다.")); - - channel.addChannelUser(user); - saveChannelData(); - } - - @Override - public void removeMember(UUID channelId, UUID userId) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - User user = userService.getUserById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다.")); - - channel.removeChannelUser(user); - saveChannelData(); - } - - @Override - public List getChannelMembers(UUID channelId) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - return new ArrayList<>(channel.getChannelUsers()); - } - - @Override - public void deleteChannelsCreatedByUser(UUID userId) { - channelData.entrySet().removeIf(entry -> entry.getValue().getChannelOwner().getId().equals(userId)); - saveChannelData(); - } - - @Override - public void removeUserFromAllChannels(UUID userId) { - for (Channel channel : channelData.values()) { - channel.getChannelUsers().removeIf(user -> user.getId().equals(userId)); - } - saveChannelData(); - } -} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/service/file/FileMessageService.java b/src/main/java/com/sprint/mission/discodeit/service/file/FileMessageService.java deleted file mode 100644 index de522c7d..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/file/FileMessageService.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.sprint.mission.discodeit.service.file; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.entity.Message; -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.MessageService; -import com.sprint.mission.discodeit.service.UserService; - -import java.io.*; -import java.text.SimpleDateFormat; -import java.util.*; - -public class FileMessageService implements MessageService { - private static final String FILE_PATH = "messages.ser"; - - private Map messageMap = new HashMap<>(); - private Map> channelMessagesMap = new HashMap<>(); - private Map> userMessagesMap = new HashMap<>(); - - private final UserService userService; - private final ChannelService channelService; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM월 dd일 a hh시 mm분 ss초"); - - public FileMessageService(UserService userService, ChannelService channelService) { - this.userService = userService; - this.channelService = channelService; - loadData(); - } - - private void loadData() { - File file = new File(FILE_PATH); - if (!file.exists()) return; - - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - messageMap = (Map) ois.readObject(); - channelMessagesMap = (Map>) ois.readObject(); - userMessagesMap = (Map>) ois.readObject(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void saveData() { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) { - oos.writeObject(messageMap); - oos.writeObject(channelMessagesMap); - oos.writeObject(userMessagesMap); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public Message createMessage(UUID channelId, UUID senderId, String content) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - User sender = userService.getUserById(senderId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다. senderId: " + senderId)); - - if (channel.getChannelUsers().size() < 2) { - throw new IllegalStateException("채널에 최소 두 명 이상의 유저가 있어야 메시지를 보낼 수 있습니다."); - } - if (!channel.getChannelUsers().contains(sender)) { - throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); - } - - Message message = new Message(content, channelId, senderId); - messageMap.put(message.getId(), message); - channelMessagesMap.computeIfAbsent(channelId, k -> new ArrayList<>()).add(message); - userMessagesMap.computeIfAbsent(senderId, k -> new ArrayList<>()).add(message); - - saveData(); - return message; - } - - @Override - public List getMessagesBySenderInChannel(UUID channelId, UUID senderId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(senderId))) { - throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); - } - - return channelMessagesMap.getOrDefault(channelId, Collections.emptyList()) - .stream() - .filter(m -> m.getSenderId().equals(senderId)) - .toList(); - } - - @Override - public List getMessagesByReceiverInChannel(UUID channelId, UUID receiverId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(receiverId))) { - throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); - } - - return channelMessagesMap.getOrDefault(channelId, Collections.emptyList()) - .stream() - .filter(m -> !m.getSenderId().equals(receiverId)) - .toList(); - } - - @Override - public List getAllSentMessages(UUID senderId) { - return userMessagesMap.getOrDefault(senderId, Collections.emptyList()); - } - - @Override - public List getAllReceivedMessages(UUID receiverId) { - List myChannels = channelService.getAllChannels().stream() - .filter(ch -> ch.getChannelUsers().stream().anyMatch(u -> u.getId().equals(receiverId))) - .map(Channel::getId) - .toList(); - - List received = new ArrayList<>(); - for (UUID chId : myChannels) { - List messages = channelMessagesMap.getOrDefault(chId, Collections.emptyList()); - for (Message msg : messages) { - if (!msg.getSenderId().equals(receiverId)) { - received.add(msg); - } - } - } - return received; - } - - @Override - public void updateMessage(UUID messageId, UUID senderId, String newContent) { - Message msg = messageMap.get(messageId); - if (msg == null) throw new IllegalArgumentException("메시지를 찾을 수 없습니다."); - if (!msg.getSenderId().equals(senderId)) { - throw new SecurityException("해당 메시지를 수정할 권한이 없습니다."); - } - msg.updateContent(newContent); - saveData(); - } - - @Override - public void deleteMessage(UUID messageId, UUID senderId) { - Message msg = messageMap.get(messageId); - if (msg == null) throw new IllegalArgumentException("메시지를 찾을 수 없습니다."); - if (!msg.getSenderId().equals(senderId)) { - throw new SecurityException("해당 메시지를 삭제할 권한이 없습니다."); - } - messageMap.remove(messageId); - channelMessagesMap.getOrDefault(msg.getChannelId(), new ArrayList<>()).remove(msg); - userMessagesMap.getOrDefault(senderId, new ArrayList<>()).remove(msg); - saveData(); - } - - @Override - public List getAllMessagesInChannel(UUID channelId, UUID requesterId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(requesterId))) { - throw new SecurityException("채널에 접근할 수 있는 권한이 없습니다."); - } - - return channelMessagesMap.getOrDefault(channelId, Collections.emptyList()); - } - - public Optional getMessageById(UUID messageId) { - return Optional.ofNullable(messageMap.get(messageId)); - } - - public String formatMessage(Message message) { - boolean isEdited = !message.getCreatedAt().equals(message.getUpdatedAt()); - long timestamp = isEdited ? message.getUpdatedAt() : message.getCreatedAt(); - String formattedDate = DATE_FORMAT.format(new Date(timestamp)); - - Optional senderOpt = userService.getUserById(message.getSenderId()); - String senderName = senderOpt.map(User::getUsername).orElse("알 수 없음"); - String senderEmail = senderOpt.map(User::getEmail).orElse("이메일 없음"); - - String channelName = channelService.getChannelById(message.getChannelId()) - .map(Channel::getChannelName) - .orElse("알 수 없음"); - - return String.format("[%s] %s, 보낸사람: %s(%s), 내용: \"%s\"", - channelName, formattedDate, senderName, senderEmail, message.getContent()); - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/service/file/FileUserService.java b/src/main/java/com/sprint/mission/discodeit/service/file/FileUserService.java deleted file mode 100644 index f404a71a..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/file/FileUserService.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.sprint.mission.discodeit.service.file; - -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; - -import java.io.*; -import java.util.*; - -public class FileUserService implements UserService { - private static final String FILE_PATH = "users.ser"; - private final Map userData; - private ChannelService channelService; - - public FileUserService() { - this.userData = loadUserData(); // 파일에서 데이터 로드 - } - - public void setChannelService(ChannelService channelService) { - this.channelService = channelService; - } - - private void saveUserData() { - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) { - oos.writeObject(userData); // 데이터를 파일에 저장 - } catch (IOException e) { - e.printStackTrace(); - } - } - - private Map loadUserData() { - File file = new File(FILE_PATH); - if (!file.exists()) { - return new HashMap<>(); // 파일이 없으면 빈 Map 반환 - } - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - return (Map) ois.readObject(); // 파일에서 데이터 읽기 - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - return new HashMap<>(); // 오류가 발생하면 빈 Map 반환 - } - } - - @Override - public User createUser(String username, String email) { - if (isEmailDuplicate(email)) { - throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + email); - } - User user = new User(username, email); - userData.put(user.getId(), user); - saveUserData(); - return user; - } - - @Override - public Optional getUserById(UUID id) { - return Optional.ofNullable(userData.get(id)); - } - - @Override - public List getAllUsers() { - return new ArrayList<>(userData.values()); - } - - public void updateUserName(UUID id, String name) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - user.updateName(name); - saveUserData(); - } - - public void updateUserEmail(UUID id, String email) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - - if (user.getEmail().equalsIgnoreCase(email)) { - throw new IllegalArgumentException("기존과 동일한 이메일입니다."); - } - - if (isEmailDuplicate(email)) { - throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + email); - } - - user.updateEmail(email); - saveUserData(); - } - - @Override - public void deleteUser(UUID id) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - - if (channelService != null) { - channelService.deleteChannelsCreatedByUser(id); - channelService.removeUserFromAllChannels(id); - } - - userData.remove(id); - saveUserData(); - } - - private boolean isEmailDuplicate(String email) { - return userData.values().stream() - .anyMatch(user -> user.getEmail().equalsIgnoreCase(email)); // 이메일 중복 검사 - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfChannelService.java deleted file mode 100644 index b3bf9973..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfChannelService.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public class JcfChannelService implements ChannelService { - private final List channels = new ArrayList<>(); - -// private final JcfUserService userService; -// 구현체 직접 의존 -> 인터페이스 의존 -// //JcfChannelService에서 JcfUserService를 의존성으로 추가하고, 그 안에 저장된 유저를 가져온다. -// public JcfChannelService(JcfUserService userService) { -// this.userService = userService; -// } - - private final UserService userService; - public JcfChannelService(UserService userService) { - this.userService = userService; - } - - @Override - public Channel createChannel(String channelName, User ownerUser) { - boolean isDuplicate = channels.stream() - .anyMatch(c -> c.getChannelOwner().getId().equals(ownerUser.getId()) - // anyMatch() : 최소한 한 개의 요소가 주어진 조건에 만족하는지 | 하나라도 만족하면 true - && c.getChannelName().equals(channelName)); - - if (isDuplicate) { - throw new IllegalArgumentException("이미 같은 이름의 채널이 존재합니다. 다른 이름을 설정해주세요."); - } - - List members = new ArrayList<>(); - members.add(ownerUser); - Channel channel = new Channel(channelName, ownerUser, members); - channels.add(channel); - return channel; - } - - @Override - public Optional getChannelById(UUID channelId) { - return channels.stream() - .filter(e -> e.getId().equals(channelId)) - .findFirst(); - //.orElse(null); - } - - @Override - public List getAllChannels() { - return new ArrayList<>(channels); - } - - @Override - public void updateChannelName(UUID channelId, String newChannelName) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - - if (channel.getChannelName().equals(newChannelName)) { - throw new IllegalArgumentException("채널 이름이 기존과 동일합니다. 다른 이름을 입력해주세요."); - } - - UUID ownerId = channel.getChannelOwner().getId(); // 채널소유자 기준으로 채널명 중복검사 - boolean isDuplicate = channels.stream() - .anyMatch(c -> c.getChannelOwner().getId().equals(ownerId) - && c.getChannelName().equals(newChannelName)); - - if (isDuplicate) { - throw new IllegalArgumentException("해당 채널명은 이미 사용 중입니다."); - } - channel.updateChannelName(newChannelName); - } - - @Override - public void deleteChannel(UUID channelId) { - //channels.remove(getChannelById(channelId)); - channels.removeIf(e -> e.getId().equals(channelId)); - } - - @Override - public void addMember(UUID channelId, UUID userId) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - User user = userService.getUserById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다.")); - - channel.addChannelUser(user); - } - - @Override - public void removeMember(UUID channelId, UUID userId) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - User user = userService.getUserById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다. userId: " + userId)); - - channel.removeChannelUser(user); - } - - @Override - public List getChannelMembers(UUID channelId) { - Channel channel = getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채널 ID입니다.")); - return new ArrayList<>(channel.getChannelUsers()); - } - - @Override - public void deleteChannelsCreatedByUser(UUID userId) { - channels.removeIf(channel -> channel.getChannelOwner().getId().equals(userId)); - } - - @Override - public void removeUserFromAllChannels(UUID userId) { - for (Channel channel : channels) { - channel.getChannelUsers().removeIf(user -> user.getId().equals(userId)); - } - } - - /* [refactoring] - 서비스 계층에서 유저나 채널이 null일 수 있는 경우, Optional를 사용하는 편인가요? - CRUD 모든 메서드에 null 체크를 넣는 건 코드가 장황해질 수 있는데, 보통은 어떻게 처리하나요? - - private Optional findChannelById(UUID channelId) { - return channels.stream() - .filter(c -> c.getId().equals(channelId)) - .findFirst(); - } - - @Override - public Channel getChannelById(UUID channelId) { - return findChannelById(channelId).orElse(null); - } - - =================================================================================== - 커스텀 예외처리 updateUserName() 에서 null체크 생략 가능 -@Override -public User getUserById(UUID id) { - return data.stream() - .filter(e -> e.getId().equals(id)) - .findFirst() - .orElseThrow(() -> new UserNotFoundException(id)); // -} - -public void updateUserName(UUID id, String name) { - User user = getUserById(id); // 못 찾으면 여기서 예외 던짐 -// if (user == null) { -// throw new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id); -// } - user.updateName(name); -} - - */ -} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfMessageService.java b/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfMessageService.java deleted file mode 100644 index 42c76553..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfMessageService.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import com.sprint.mission.discodeit.entity.Channel; -import com.sprint.mission.discodeit.entity.Message; -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.MessageService; -import com.sprint.mission.discodeit.service.UserService; - -import java.text.SimpleDateFormat; -import java.util.*; - -public class JcfMessageService implements MessageService { - private final Map messageMap = new HashMap<>(); - private final Map> channelMessagesMap = new HashMap<>(); - private final Map> userMessagesMap = new HashMap<>(); - - private final UserService userService; - private final ChannelService channelService; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM월 dd일 a hh시 mm분 ss초"); - - public JcfMessageService(UserService userService, ChannelService channelService) { - this.userService = userService; - this.channelService = channelService; - } - - @Override - public Message createMessage(UUID channelId, UUID senderId, String content) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - User sender = userService.getUserById(senderId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저 ID입니다. senderId: " + senderId)); - - if (channel == null || sender == null) { - throw new IllegalArgumentException("채널 또는 유저가 존재하지 않습니다."); - } - if (channel.getChannelUsers().size() < 2) { - throw new IllegalStateException("채널에 최소 두 명 이상의 유저가 있어야 메시지를 보낼 수 있습니다."); - } - if (!channel.getChannelUsers().contains(sender)) { - throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); - } - - Message message = new Message(content, channelId, senderId); - messageMap.put(message.getId(), message); - - channelMessagesMap.computeIfAbsent(channelId, k -> new ArrayList<>()).add(message); -// if (!channelMessagesMap.containsKey(channelId)) { -// channelMessagesMap.put(channelId, new ArrayList<>()); -// } -// channelMessagesMap.get(channelId).add(message); - - userMessagesMap.computeIfAbsent(senderId, k -> new ArrayList<>()).add(message); - return message; - } - - @Override - public List getMessagesBySenderInChannel(UUID channelId, UUID senderId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(senderId))) - // noneMatch() : 모든 요소들이 주어진 조건을 만족하지 않는지 | 아무도 조건을 만족하지 않아야 true - throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); - - return channelMessagesMap.getOrDefault(channelId, Collections.emptyList()) - .stream() - .filter(m -> m.getSenderId().equals(senderId)) - .toList(); - -// List messages; -// if (channelMessagesMap.containsKey(channelId)) { -// messages = channelMessagesMap.get(channelId); -// } else { -// messages = new ArrayList<>(); // null이 아닌 안전한 리스트 사용 .stream() 에서의 NPE 방지 -// } -// -// return messages.stream() -// .filter(m -> m.getSenderId().equals(senderId)) -// .toList(); - } - - @Override - public List getMessagesByReceiverInChannel(UUID channelId, UUID receiverId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(receiverId))) - throw new IllegalArgumentException("해당 유저는 이 채널의 멤버가 아닙니다."); - - return channelMessagesMap.getOrDefault(channelId, Collections.emptyList()) - .stream() - .filter(m -> !m.getSenderId().equals(receiverId)) - .toList(); - } - - @Override - public List getAllSentMessages(UUID senderId) { - return userMessagesMap.getOrDefault(senderId, Collections.emptyList()); - } - - @Override - public List getAllReceivedMessages(UUID receiverId) { - List myChannels = channelService.getAllChannels().stream() - .filter(ch -> ch.getChannelUsers().stream().anyMatch(u -> u.getId().equals(receiverId))) // 해당 유저가 속한 채널 필터링 - .map(channel -> channel.getId()) -// .flatMap(chId -> channelMessagesMap.getOrDefault(chId, Collections.emptyList()).stream()) -// .filter(msg -> !msg.getSenderId().equals(receiverId)) - .toList(); - - List received = new ArrayList<>(); - for (UUID chId : myChannels) { - List messages = channelMessagesMap.getOrDefault(chId, Collections.emptyList()); //각 채널에 있는 메시지를 순회 - for (Message msg : messages) { - if (!msg.getSenderId().equals(receiverId)) { // 보낸 사람이 receiver가 아닌 메시지만 필터링 - received.add(msg); - } - } - } - return received; - } - - @Override - public void updateMessage(UUID messageId, UUID senderId, String newContent) { - Message msg = messageMap.get(messageId); - if (msg == null) throw new IllegalArgumentException("메시지를 찾을 수 없습니다."); - if (!msg.getSenderId().equals(senderId)) { - throw new SecurityException("해당 메시지를 수정할 권한이 없습니다."); - } - msg.updateContent(newContent); - } - - public Optional getMessageById(UUID messageId) { - return Optional.ofNullable(messageMap.get(messageId)); - } - - @Override - public void deleteMessage(UUID messageId, UUID senderId) { - Message msg = messageMap.get(messageId); - if (msg == null) throw new IllegalArgumentException("메시지를 찾을 수 없습니다."); - if (!msg.getSenderId().equals(senderId)) { - throw new SecurityException("해당 메시지를 삭제할 권한이 없습니다."); - } - messageMap.remove(messageId); - channelMessagesMap.getOrDefault(msg.getChannelId(), new ArrayList<>()).remove(msg); - userMessagesMap.getOrDefault(senderId, new ArrayList<>()).remove(msg); - } - - @Override - public List getAllMessagesInChannel(UUID channelId, UUID requesterId) { - Channel channel = channelService.getChannelById(channelId) - .orElseThrow(() -> new IllegalArgumentException("해당 채널이 존재하지 않습니다.")); - if (channel.getChannelUsers().stream().noneMatch(u -> u.getId().equals(requesterId))) - throw new SecurityException("채널에 접근할 수 있는 권한이 없습니다."); - - return channelMessagesMap.getOrDefault(channelId, Collections.emptyList()); - } - - // 출력- 문자열 포맷팅 - public String formatMessage(Message message) { - boolean isEdited = !message.getCreatedAt().equals(message.getUpdatedAt()); - long timestamp = isEdited ? message.getUpdatedAt() : message.getCreatedAt(); - String formattedDate = DATE_FORMAT.format(new Date(timestamp)); - - Optional senderOpt = userService.getUserById(message.getSenderId()); - String senderName = senderOpt.map(User::getUsername).orElse("알 수 없음"); - String senderEmail = senderOpt.map(User::getEmail).orElse("이메일 없음"); - - String channelName = channelService.getChannelById(message.getChannelId()) - .map(Channel::getChannelName) - .orElse("알 수 없음"); - - return String.format("[%s] %s, 보낸사람: %s(%s), 내용: \"%s\"", - channelName, formattedDate, senderName, senderEmail, message.getContent()); - } -} \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfUserService.java b/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfUserService.java deleted file mode 100644 index 083e8e2f..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfUserService.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.ChannelService; -import com.sprint.mission.discodeit.service.UserService; - -import java.util.*; - -public class JcfUserService implements UserService { - private final List data = new ArrayList<>(); - /* - JcfUserService → JcfChannelService에 의존 - 유저가 탈퇴할 때 그 유저가 만든 채널도 삭제해야한다. 속한 채널에서도 그 유저가 사라져야 한다. - JcfChannelService → JcfUserService에 의존 - 사용자 ID로 User 객체 가져오기 위해 - 순환참조 발생 - */ - //private JcfChannelService channelService; - private ChannelService channelService; - public void setChannelService(ChannelService channelService) { - this.channelService = channelService; - } - - @Override - public User createUser(String username, String email) { - if (isEmailDuplicate(email)) { - throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + email); - } - User user = new User(username, email); - data.add(user); - return user; - } - - @Override - public Optional getUserById(UUID id) { - return data.stream() - .filter(e -> e.getId().equals(id)) - .findFirst(); // UUID는 유일하므로 첫 번째 결과 반환. 만약 비어있으면 Optional.empty()를 반환 - //.orElse(null); // 유저가 없으면 null 반환 - // .orElseThrow(() -> new IllegalArgumentException("조회할 User를 찾지 못했습니다.")); - /* - //직관적인 foreach문 - for (User user : users) { - if (user.getId().equals(id)) { - return user; - } - } - throw new IllegalArgumentException("조회할 User를 찾지 못했습니다."); - */ - } - - @Override - public List getAllUsers() { - return new ArrayList<>(data); - } - - public void updateUserName(UUID id, String name) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - user.updateName(name); - } - - public void updateUserEmail(UUID id, String email) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - - if (user.getEmail().equalsIgnoreCase(email)) { - throw new IllegalArgumentException("기존과 동일한 이메일입니다."); - } - - if (isEmailDuplicate(email)) { - throw new IllegalArgumentException("이미 존재하는 이메일입니다: " + email); - } - - user.updateEmail(email); - } - - @Override - public void deleteUser(UUID id) { // 유저가 삭제되면, 그 유저가 만든 채널도 함께 삭제되도록 연동이 필요 - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - - if (channelService != null) { - channelService.deleteChannelsCreatedByUser(id); // 유저가 만든 채널 삭제 - channelService.removeUserFromAllChannels(id); // 유저가 참여한 채널에서 탈퇴 - } - - data.remove(user); - } - - private boolean isEmailDuplicate(String email) { - return data.stream() - .anyMatch(user -> user.getEmail().equalsIgnoreCase(email)); - } - // 시간복잡도 - // Create - List.add() -> O(1) ~ O(N) - // Read - getUserById() ... filter() -> O(N) & getAllUsers() -> new ArrayList<> 리스트 복사 O(N) - // Update - getUserById() ... filter() -> O(N) - // Delete - getUserById() ... filter() -> O(N) -} diff --git a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfUserServiceUsingMap.java b/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfUserServiceUsingMap.java deleted file mode 100644 index c1dd9370..00000000 --- a/src/main/java/com/sprint/mission/discodeit/service/jcf/JcfUserServiceUsingMap.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.sprint.mission.discodeit.service.jcf; - -import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.service.UserService; - -import java.util.*; - -public class JcfUserServiceUsingMap implements UserService { - - private final Map data; - - public JcfUserServiceUsingMap() { - this.data = new HashMap<>(); - } - - @Override - public User createUser(String username, String email) { - User user = new User(username, email); - data.put(user.getId(), user); - return user; - } - - @Override - public Optional getUserById(UUID id) { - return Optional.ofNullable(data.get(id)); - //return Optional.ofNullable(users.get(id)).orElseThrow(() -> new IllegalArgumentException("조회할 User를 찾지 못했습니다.")); - } - - @Override - public List getAllUsers() { - return new ArrayList<>(data.values()); - } - - @Override - public void updateUserName(UUID id, String name) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); // 존재하는지 확인 - //User user = Optional.ofNullable(users.get(id)) - // .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); - user.updateName(name); - } - - @Override - public void updateUserEmail(UUID id, String email) { - User user = getUserById(id) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 유저를 찾을 수 없습니다: " + id)); // 존재하는지 확인 - user.updateEmail(email); - } - - @Override - public void deleteUser(UUID id) { - if (data.remove(id) == null) { - throw new IllegalArgumentException("삭제 실패: 해당 ID (" + id + ")의 유저를 찾을 수 없습니다."); - } - } - - // 시간복잡도 - // Create - data.put() -> O(1) - // Read - data.get() -> O(1) & data.values() -> O(N) + new ArrayList<>() -> O(N) - // Update - data.get() -> O(1) - // Delete - date.remove() -> O(1) -} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 00000000..0d2f3a95 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +discodeit: + repository: + type: file # jcf | file + file-directory: .discodeit \ No newline at end of file diff --git a/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java b/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java new file mode 100644 index 00000000..3a987a21 --- /dev/null +++ b/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() { + } + +}