diff --git a/.ebextensions-dev/00-makeFiles.config b/.ebextensions-dev/00-makeFiles.config new file mode 100644 index 00000000..3d644169 --- /dev/null +++ b/.ebextensions-dev/00-makeFiles.config @@ -0,0 +1,12 @@ +files: + "/sbin/appstart" : + mode: "000755" + owner: webapp + group: webapp + content: | + #!/usr/bin/env bash + JAR_PATH=/var/app/current/application.jar + + # run app + killall java + java -Dfile.encoding=UTF-8 -Dspring.profiles.active=dev -jar $JAR_PATH \ No newline at end of file diff --git a/.ebextensions-dev/01-set-timezone.config b/.ebextensions-dev/01-set-timezone.config new file mode 100644 index 00000000..869275c7 --- /dev/null +++ b/.ebextensions-dev/01-set-timezone.config @@ -0,0 +1,3 @@ +commands: + set_time_zone: + command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime \ No newline at end of file diff --git a/.ebextensions-release/00-makeFiles.config b/.ebextensions-release/00-makeFiles.config new file mode 100644 index 00000000..8f5efe73 --- /dev/null +++ b/.ebextensions-release/00-makeFiles.config @@ -0,0 +1,12 @@ +files: + "/sbin/appstart" : + mode: "000755" + owner: webapp + group: webapp + content: | + #!/usr/bin/env bash + JAR_PATH=/var/app/current/application.jar + + # run app + killall java + java -Dfile.encoding=UTF-8 -Dspring.profiles.active=release -jar $JAR_PATH \ No newline at end of file diff --git a/.ebextensions-release/01-set-timezone.config b/.ebextensions-release/01-set-timezone.config new file mode 100644 index 00000000..869275c7 --- /dev/null +++ b/.ebextensions-release/01-set-timezone.config @@ -0,0 +1,3 @@ +commands: + set_time_zone: + command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime \ No newline at end of file diff --git a/.github/workflows/create_jira_issue.yml b/.github/workflows/create_jira_issue.yml new file mode 100644 index 00000000..dbc15408 --- /dev/null +++ b/.github/workflows/create_jira_issue.yml @@ -0,0 +1,24 @@ +name: Create Jira issue # 1 +on: # 2 + issues: + types: [opened] + +jobs: # 3 + create-issue: # 4 + name: Create Jira issue # 5 + runs-on: ubuntu-latest # 6 + steps: # 7 + - name: Login + uses: atlassian/gajira-login@v3 # 8 + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} # 9 + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + + - name: Create Issue + uses: atlassian/gajira-create@v3 + with: + project: MM # 10 - 프로젝트 key + issuetype: Task # 11 - 이슈 타입 + summary: '${{ github.event.issue.title }}' + description: '${{ github.event.issue.html_url }}' diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml new file mode 100644 index 00000000..38a1939e --- /dev/null +++ b/.github/workflows/dev_deploy.yml @@ -0,0 +1,62 @@ +name: UMC Dev CI/CD + +on: + pull_request: + types: [closed] + workflow_dispatch: # (2).수동 실행도 가능하도록 + +jobs: + build: + runs-on: ubuntu-latest # (3).OS환경 + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' + + steps: + - name: Checkout + uses: actions/checkout@v2 # (4).코드 check out + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 # (5).자바 설치 + distribution: 'adopt' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash # (6).권한 부여 + + - name: Build with Gradle + run: ./gradlew clean build -x test + shell: bash # (7).build시작 + + - name: Get current time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH-mm-ss + utcOffset: "+09:00" # (8).build시점의 시간확보 + + - name: Show Current Time + run: echo "CurrentTime=$" + shell: bash # (9).확보한 시간 보여주기 + + - name: Generate deployment package + run: | + mkdir -p deploy + cp build/libs/*.jar deploy/application.jar + cp Procfile deploy/Procfile + cp -r .ebextensions-dev deploy/.ebextensions + cp -r .platform deploy/.platform + cd deploy && zip -r deploy.zip . + + - name: Beanstalk Deploy + uses: einaregilsson/beanstalk-deploy@v20 + with: + aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} + aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} + application_name: moim-dev + environment_name: Moim-dev-env + version_label: github-action-${{ steps.current-time.outputs.formattedTime }} + region: ap-northeast-2 + deployment_package: deploy/deploy.zip + wait_for_deployment: false + diff --git a/.github/workflows/release_deploy.yml b/.github/workflows/release_deploy.yml new file mode 100644 index 00000000..0b38eab9 --- /dev/null +++ b/.github/workflows/release_deploy.yml @@ -0,0 +1,62 @@ +name: UMC Dev CI/CD + +on: + pull_request: + types: [closed] + workflow_dispatch: # (2).수동 실행도 가능하도록 + +jobs: + build: + runs-on: ubuntu-latest # (3).OS환경 + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' + + steps: + - name: Checkout + uses: actions/checkout@v2 # (4).코드 check out + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 # (5).자바 설치 + distribution: 'adopt' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash # (6).권한 부여 + + - name: Build with Gradle + run: ./gradlew clean build -x test + shell: bash # (7).build시작 + + - name: Get current time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH-mm-ss + utcOffset: "+09:00" # (8).build시점의 시간확보 + + - name: Show Current Time + run: echo "CurrentTime=$" + shell: bash # (9).확보한 시간 보여주기 + + - name: Generate deployment package + run: | + mkdir -p deploy + cp build/libs/*.jar deploy/application.jar + cp Procfile deploy/Procfile + cp -r .ebextensions-release deploy/.ebextensions + cp -r .platform deploy/.platform + cd deploy && zip -r deploy.zip . + + - name: Beanstalk Deploy + uses: einaregilsson/beanstalk-deploy@v20 + with: + aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} + aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} + application_name: moim-release + environment_name: Moim-release-env + version_label: github-action-${{ steps.current-time.outputs.formattedTime }} + region: ap-northeast-2 + deployment_package: deploy/deploy.zip + wait_for_deployment: false + diff --git a/.gitignore b/.gitignore index c2065bc2..ba3569c7 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ out/ ### VS Code ### .vscode/ + +src/main/generated + + diff --git a/.platform/nginx.conf b/.platform/nginx.conf new file mode 100644 index 00000000..612092e1 --- /dev/null +++ b/.platform/nginx.conf @@ -0,0 +1,63 @@ +user nginx; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; +worker_processes auto; +worker_rlimit_nofile 33282; + +events { + use epoll; + worker_connections 1024; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + include conf.d/*.conf; + + map $http_upgrade $connection_upgrade { + default "upgrade"; + } + + upstream springboot { + server 127.0.0.1:8080; + keepalive 1024; + } + + server { + listen 80 default_server; + listen [::]:80 default_server; + + location / { + proxy_pass http://springboot; + # CORS 관련 헤더 추가 + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type'; + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/access.log main; + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + gzip off; + gzip_comp_level 4; + + # Include the Elastic Beanstalk generated locations + include conf.d/elasticbeanstalk/healthd.conf; + } +} \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..58dab8d4 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: appstart \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8fca5f51..b668dbe1 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,10 @@ repositories { mavenCentral() } +ext { + set('springCloudVersion', "2023.0.2") +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' @@ -31,8 +35,66 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // websocket + implementation 'org.springframework.boot:spring-boot-starter-websocket' + + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + + implementation 'org.springframework.boot:spring-boot-starter-mail' + + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // AWS S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // Querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // fcm + implementation 'com.google.firebase:firebase-admin:9.1.0' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } } tasks.named('test') { useJUnitPlatform() } + +jar { + enabled = false +} + +// Querydsl 설정 +def generated = 'src/main/generated' + +// querydsl QClass 파일 생성 위치 지정 +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +// querydsl QClass 위치 추가 +sourceSets { + main.java.srcDirs += [ generated ] +} + +// gradle clean 시에 QClass 디렉토리 삭제 +clean { + delete file(generated) +} diff --git a/src/main/java/com/dev/moim/MoimApplication.java b/src/main/java/com/dev/moim/MoimApplication.java index 7cb59e10..e212028a 100644 --- a/src/main/java/com/dev/moim/MoimApplication.java +++ b/src/main/java/com/dev/moim/MoimApplication.java @@ -1,8 +1,17 @@ package com.dev.moim; +import com.dev.moim.global.security.feign.config.OauthProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling +@EnableConfigurationProperties(OauthProperties.class) +@EnableJpaAuditing +@EnableFeignClients @SpringBootApplication public class MoimApplication { @@ -10,4 +19,4 @@ public static void main(String[] args) { SpringApplication.run(MoimApplication.class, args); } -} +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/account/controller/AuthController.java b/src/main/java/com/dev/moim/domain/account/controller/AuthController.java new file mode 100644 index 00000000..c06845a0 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/controller/AuthController.java @@ -0,0 +1,150 @@ +package com.dev.moim.domain.account.controller; + +import com.dev.moim.domain.account.dto.*; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.service.AuthService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.security.annotation.AuthUser; +import com.dev.moim.global.security.annotation.ExtractToken; +import com.dev.moim.global.validation.annotation.QuitValidation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "유저 관리 관련 컨트롤러") +public class AuthController { + + private final AuthService authService; + + @PostMapping("/join") + @Operation(summary="회원 가입 API", description="회원가입 API 입니다." ) + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + @ApiResponse(responseCode = "AUTH_011", description = "이미 가입한 메일 입니다."), + @ApiResponse(responseCode = "AUTH_025", description = "providerId가 누락되었습니다."), + @ApiResponse(responseCode = "AUTH_022", description = "이미 가입한 소셜 계정입니다.") + }) + public BaseResponse join(@Valid @RequestBody JoinRequest request) { + return BaseResponse.onSuccess(authService.join(request)); + } + + @PostMapping("/login") + @Operation(summary="일반 로그인 API", description="로그인 성공 시, token 정보 반환 (개발용)" ) + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "AUTH_009", description = "비밀번호를 잘못 입력했습니다."), + @ApiResponse(responseCode = "AUTH_010", description = "인증에 실패했습니다."), + @ApiResponse(responseCode = "AUTH_021", description = "존재하지 않는 사용자입니다."), + @ApiResponse(responseCode = "AUTH_028", description = "존재하지 않는 계정입니다. 회원가입을 진행해주세요."), + @ApiResponse(responseCode = "FCM_002", description = "FCM 토큰이 누락되었습니다."), + }) + public BaseResponse localLogin(@RequestBody LoginRequest request) { + return BaseResponse.onSuccess(null); + } + + @PostMapping("/oAuth") + @Operation(summary="소셜 로그인 API", description="소셜 로그인 타입을 입력해주세요.\n [Provider] KAKAO, APPLE, GOOGLE, NAVER (개발용)") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "AUTH_016", description = "지원하지 않는 로그인 provider 입니다."), + @ApiResponse(responseCode = "AUTH_018", description = "만료된 ID 토큰 입니다."), + @ApiResponse(responseCode = "AUTH_019", description = "유효하지 않은 ID 토큰 입니다."), + @ApiResponse(responseCode = "FCM_002", description = "FCM 토큰이 누락되었습니다."), + @ApiResponse(responseCode = "AUTH_028", description = "존재하지 않는 계정입니다. 회원가입을 진행해주세요."), + @ApiResponse(responseCode = "AUTH_029", description = "OIDC ID 토큰 공개키를 받아오는데 실패했습니다.") + }) + public BaseResponse oAuthLogin( + @RequestBody OAuthLoginRequest request + ) { + return BaseResponse.onSuccess(null); + } + + @GetMapping("/reissueToken") + @Operation(summary="토큰 재발급 API", description="AccessToken의 유효 기간이 만료된 경우, Authorization 헤더에 RefreshToken을 담아서 요청을 보내면 AccessToken과 RefreshToken 재발급.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "AUTH_001", description = "토큰이 만료되었습니다."), + @ApiResponse(responseCode = "AUTH_002", description = "토큰이 유효하지 않습니다."), + @ApiResponse(responseCode = "AUTH_021", description = "존재하지 않는 사용자입니다.") + }) + public BaseResponse reissueToken(@ExtractToken @Parameter(name = "refreshToken", hidden = true) String refreshToken) { + return BaseResponse.onSuccess(authService.reissueToken(refreshToken)); + } + + @GetMapping("/logout") + @Operation(summary="로그아웃 API", description="로그아웃 후, 기존 유효한 토큰 무효화 (개발용)" ) + public BaseResponse signOut( + @ExtractToken @Parameter(name = "accessToken", hidden = true) String accessToken + ) { + return BaseResponse.onSuccess("로그아웃 성공"); + } + + @PostMapping("/emails/send") + @Operation(summary="이메일 인증 코드 전송 요청 API", description="이메일 인증 번호 전송을 요청하는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "EMAIL_001", description = "이메일 전송에 실패했습니다.") + }) + public BaseResponse sendCode(@RequestBody EmailDTO request) { + return BaseResponse.onSuccess(authService.sendCode(request)); + } + + @PostMapping("/emails/verify") + @Operation(summary="이메일 코드 인증 요청 API", description="이메일 인증 코드 일치 여부를 확인해주는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "EMAIL_002", description = "이메일 인증 코드가 일치하지 않습니다."), + @ApiResponse(responseCode = "EMAIL_003", description = "유저 이메일에 해당하는 이메일 코드가 저장되어있지 않습니다. 재요청을 시도해주세요."), + @ApiResponse(responseCode = "EMAIL_004", description = "이메일 인증에 실패했습니다.") + }) + public BaseResponse verifyCode(@RequestBody EmailVerificationCodeDTO request) { + return BaseResponse.onSuccess(authService.verifyCode(request)); + } + + @PutMapping("/password") + @Operation(summary="비밀번호 변경", description="비밀번호 분실 시, 이메일 검증에 성공한 경우 새로운 비밀번호로 변경할 수 있는 API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + + }) + public BaseResponse updatePassword( + @Valid @RequestBody UpdatePasswordDTO request + ) { + authService.updatePassword(request); + return BaseResponse.onSuccess(null); + } + + @DeleteMapping("/quit") + @Operation(summary="회원 탈퇴", description="회원 탈퇴 API입니다. 모임장 권한을 가진 유저는 회원 탈퇴 전에 권한 위임을 진행해주세요.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + public BaseResponse quit(@QuitValidation @AuthUser User user + ) { + authService.quit(user); + return BaseResponse.onSuccess(null); + } + + @PostMapping("/inquiries") + @Operation(summary="의견 및 문의 메일 보내기", description="유저가 모임 서비스에 대한 의견 및 문의 사항을 보내는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + public BaseResponse submitInquiry( + @AuthUser User user, + @RequestBody InquiryDTO request + ) { + authService.submitInquiry(user, request); + return BaseResponse.onSuccess("의견 및 문의 메일 보내기 성공"); + } +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/EmailDTO.java b/src/main/java/com/dev/moim/domain/account/dto/EmailDTO.java new file mode 100644 index 00000000..6799ecae --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/EmailDTO.java @@ -0,0 +1,6 @@ +package com.dev.moim.domain.account.dto; + +public record EmailDTO( + String email +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/EmailVerificationCodeDTO.java b/src/main/java/com/dev/moim/domain/account/dto/EmailVerificationCodeDTO.java new file mode 100644 index 00000000..16907c05 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/EmailVerificationCodeDTO.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.account.dto; + +public record EmailVerificationCodeDTO( + String email, + String code +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/EmailVerificationResultDTO.java b/src/main/java/com/dev/moim/domain/account/dto/EmailVerificationResultDTO.java new file mode 100644 index 00000000..6380bcef --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/EmailVerificationResultDTO.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.account.dto; + +public record EmailVerificationResultDTO( + String email, + boolean isCodeValid +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/InquiryDTO.java b/src/main/java/com/dev/moim/domain/account/dto/InquiryDTO.java new file mode 100644 index 00000000..230ea916 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/InquiryDTO.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.account.dto; + +public record InquiryDTO( + String content, + String replyEmail +) { +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/account/dto/JoinRequest.java b/src/main/java/com/dev/moim/domain/account/dto/JoinRequest.java new file mode 100644 index 00000000..8f3dcd9a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/JoinRequest.java @@ -0,0 +1,41 @@ +package com.dev.moim.domain.account.dto; + +import com.dev.moim.domain.account.entity.enums.Gender; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.global.validation.annotation.FcmTokenValidation; +import com.dev.moim.global.validation.annotation.LocalAccountValidation; +import com.dev.moim.global.validation.annotation.JoinPasswordValidation; +import com.dev.moim.global.validation.annotation.OAuthAccountValidation; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import org.springframework.lang.NonNull; + +import java.time.LocalDate; + +// @JoinFcmTokenValidtaion +@JsonInclude(JsonInclude.Include.NON_NULL) +@OAuthAccountValidation +@LocalAccountValidation +@JoinPasswordValidation +public record JoinRequest( + @Schema(description = "로그인 타입", defaultValue = "KAKAO", allowableValues = {"LOCAL", "KAKAO", "APPLE", "GOOGLE"}) + @NonNull + Provider provider, + String providerId, + @NotBlank String fcmToken, + @NotBlank + String nickname, + @NotBlank + String email, + String password, + @Schema(description = "성별", defaultValue = "FEMALE", allowableValues = {"FEMALE", "MALE"}, nullable = true) + Gender gender, + @Schema(nullable = true) + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate birth, + @Schema(nullable = true) + String residence +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/LoginRequest.java b/src/main/java/com/dev/moim/domain/account/dto/LoginRequest.java new file mode 100644 index 00000000..10b77c3c --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/LoginRequest.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.account.dto; + +public record LoginRequest( + String email, + String password, + String fcmToken +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/LoginResponseDTO.java b/src/main/java/com/dev/moim/domain/account/dto/LoginResponseDTO.java new file mode 100644 index 00000000..2e54ea46 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/LoginResponseDTO.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.account.dto; + +import com.dev.moim.domain.account.entity.enums.Provider; + +public record LoginResponseDTO( + String accessToken, + String refreshToken, + Provider provider, + String email +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/OAuthLoginRequest.java b/src/main/java/com/dev/moim/domain/account/dto/OAuthLoginRequest.java new file mode 100644 index 00000000..0484f16f --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/OAuthLoginRequest.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.account.dto; + +import com.dev.moim.domain.account.entity.enums.Provider; +import io.swagger.v3.oas.annotations.media.Schema; + +public record OAuthLoginRequest( + @Schema(description = "소셜 로그인 타입", defaultValue = "KAKAO", allowableValues = {"KAKAO", "APPLE", "GOOGLE", "NAVER"}) + Provider provider, + String token, + String fcmToken +) { +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/account/dto/OIDCDecodePayload.java b/src/main/java/com/dev/moim/domain/account/dto/OIDCDecodePayload.java new file mode 100644 index 00000000..0e777c4b --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/OIDCDecodePayload.java @@ -0,0 +1,9 @@ +package com.dev.moim.domain.account.dto; + +public record OIDCDecodePayload( + String iss, + String aud, + String sub, + String email +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/TokenResponse.java b/src/main/java/com/dev/moim/domain/account/dto/TokenResponse.java new file mode 100644 index 00000000..dde0ca7e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/TokenResponse.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.account.dto; + +import com.dev.moim.domain.account.entity.enums.Provider; + +public record TokenResponse( + String accessToken, + String refreshToken, + Provider provider +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/dto/UpdatePasswordDTO.java b/src/main/java/com/dev/moim/domain/account/dto/UpdatePasswordDTO.java new file mode 100644 index 00000000..ac74a626 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/dto/UpdatePasswordDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.account.dto; + +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.global.validation.annotation.ExistEmailValidation; +import com.dev.moim.global.validation.annotation.UpdatePasswordValidation; + +public record UpdatePasswordDTO( + Provider provider, + @ExistEmailValidation String email, + @UpdatePasswordValidation String password +) { +} diff --git a/src/main/java/com/dev/moim/domain/account/entity/SNS.java b/src/main/java/com/dev/moim/domain/account/entity/Alarm.java similarity index 53% rename from src/main/java/com/dev/moim/domain/account/entity/SNS.java rename to src/main/java/com/dev/moim/domain/account/entity/Alarm.java index 7f763f93..25798997 100644 --- a/src/main/java/com/dev/moim/domain/account/entity/SNS.java +++ b/src/main/java/com/dev/moim/domain/account/entity/Alarm.java @@ -1,15 +1,9 @@ package com.dev.moim.domain.account.entity; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,15 +15,29 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class SNS extends BaseEntity { +public class Alarm extends BaseEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String email; + private String title; + + private String content; + + private Long writerId; + + @Enumerated(EnumType.STRING) + private AlarmType alarmType; @Enumerated(EnumType.STRING) - private com.dev.moim.domain.account.entity.enums.SNSType SNSType; + private AlarmDetailType alarmDetailType; + + private Long moimId; + + private Long postId; + + private Long commentId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") diff --git a/src/main/java/com/dev/moim/domain/account/entity/User.java b/src/main/java/com/dev/moim/domain/account/entity/User.java index 5c345484..e4b991a5 100644 --- a/src/main/java/com/dev/moim/domain/account/entity/User.java +++ b/src/main/java/com/dev/moim/domain/account/entity/User.java @@ -1,24 +1,14 @@ package com.dev.moim.domain.account.entity; -import com.dev.moim.domain.account.entity.enums.Provider; -import com.dev.moim.domain.account.entity.enums.UserRank; -import com.dev.moim.domain.account.entity.enums.UserStatus; -import com.dev.moim.domain.chatting.entity.UserChattingRoom; -import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.account.entity.enums.*; +import com.dev.moim.domain.moim.entity.*; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import lombok.*; +import org.hibernate.annotations.*; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -28,38 +18,143 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@DynamicInsert +@SQLDelete(sql = "UPDATE user SET status = 'OFF', inactive_date = current_timestamp WHERE id = ?") +@SQLRestriction(value = "status = 'ON'") public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) private String email; private String password; - private String nickname; - @Enumerated(EnumType.STRING) + @Column(nullable = false) private Provider provider; + private String providerId; + private LocalDateTime inactive_date; + @Column(nullable = false) + private LocalDateTime lastAlarmTime; + + private String deviceId; + + @Column(nullable = false) + @ColumnDefault("true") + private Boolean isPushAlarm; + + @Column(nullable = false) + @ColumnDefault("true") + private Boolean isEventAlarm; + @Enumerated(EnumType.STRING) - private UserStatus userStatus; + private Gender gender; + + private LocalDate birth; + + private String residence; + + private double rating; @Enumerated(EnumType.STRING) + @ColumnDefault("'ROLE_USER'") + @Column(nullable = false) + private UserRole userRole; + + @Column(nullable = false) + @ColumnDefault("'ON'") + @Enumerated(EnumType.STRING) + private UserStatus status; + + @Enumerated(EnumType.STRING) + @ColumnDefault("'FREE'") + @Column(nullable = false) private UserRank userRank; - @OneToMany(mappedBy = "user") + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List userProfileList = new ArrayList<>(); - @OneToMany(mappedBy = "user") - private List snsList = new ArrayList<>(); - - @OneToMany(mappedBy = "user") + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) private List userMoimList = new ArrayList<>(); - @OneToMany(mappedBy = "user") - private List userChattingRoomList = new ArrayList<>(); -} + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List userPlanList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List individualPlanList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List userTodoList = new ArrayList<>(); + + @OneToMany(mappedBy = "writer", cascade = CascadeType.ALL) + private List todoList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List readPostList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List alarmList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List postReportList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List postBlockList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List postLikeList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List commentBlockList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List commentLikeList = new ArrayList<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + private List commentReportList = new ArrayList<>(); + + public void updateRating(double newRating) { + this.rating = newRating; + } + + public void changePushAlarm() { + this.isPushAlarm = !this.isPushAlarm; + } + + public void changeEventAlarm() { + this.isEventAlarm = !this.isEventAlarm; + } + + public void updatePassword(String newPassword) {this.password = newPassword;} + + public void updateDeviceId(String fcmToken) { + this.deviceId = fcmToken; + } + + public void fcmSignOut() { + this.deviceId = null; + } + + public void updateAlarmTime() { + this.lastAlarmTime = LocalDateTime.now(); + } + + public void updateUserInfo(String residence, Gender gender, LocalDate birth) { + this.residence = residence; + this.gender = gender; + this.birth = birth; + } + + @PreRemove + public void preRemove() { + for (Todo todo : todoList) { + todo.updateWriter(null); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/account/entity/UserProfile.java b/src/main/java/com/dev/moim/domain/account/entity/UserProfile.java index b173f634..0c58d2f8 100644 --- a/src/main/java/com/dev/moim/domain/account/entity/UserProfile.java +++ b/src/main/java/com/dev/moim/domain/account/entity/UserProfile.java @@ -1,57 +1,72 @@ package com.dev.moim.domain.account.entity; -import com.dev.moim.domain.account.entity.enums.Gender; -import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.moim.entity.UserMoim; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; -import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Table(name = "user_profile") +@SQLDelete(sql = "UPDATE user_profile SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?") +@SQLRestriction(value = "deleted_at IS NULL") public class UserProfile extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_profile_id") private Long id; - private String name; - - private String profile_image; - - private String residence; - @Enumerated(EnumType.STRING) - private Gender gender; + @ColumnDefault("'MAIN'") + @Column(nullable = false) + private ProfileType profileType; + + @Column(nullable = false) + private String name; - private LocalDate birth; + private String imageUrl; private String introduction; + private LocalDateTime deletedAt; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; - @OneToOne(fetch = FetchType.LAZY) - private Moim moim; -} + @OneToMany(mappedBy = "userProfile", cascade = CascadeType.REMOVE) + private List userMoimList = new ArrayList<>(); + + public void addUser(User user) { + user.getUserProfileList().add(this); + this.user = user; + } + + @PreRemove + public void preRemove() { + this.deletedAt = LocalDateTime.now(); + } + + public void updateProfileType(ProfileType profileType) { + this.profileType = profileType; + } + + public void updateUserProfile(String nickname, String introduction, String imageUrl) { + this.name = nickname; + this.introduction = introduction; + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/moim/entity/MoimTag.java b/src/main/java/com/dev/moim/domain/account/entity/UserReview.java similarity index 50% rename from src/main/java/com/dev/moim/domain/moim/entity/MoimTag.java rename to src/main/java/com/dev/moim/domain/account/entity/UserReview.java index b247db38..e44b0a1f 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/MoimTag.java +++ b/src/main/java/com/dev/moim/domain/account/entity/UserReview.java @@ -1,13 +1,7 @@ -package com.dev.moim.domain.moim.entity; +package com.dev.moim.domain.account.entity; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -19,13 +13,20 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class MoimTag extends BaseEntity { +public class UserReview extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + private Double rating; + + private String content; + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "moim_id") - private Moim moim; + @JoinColumn(name = "user_id") + private User user; + + private Long writerId; } diff --git a/src/main/java/com/dev/moim/domain/account/entity/enums/AlarmDetailType.java b/src/main/java/com/dev/moim/domain/account/entity/enums/AlarmDetailType.java new file mode 100644 index 00000000..838c1d07 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/entity/enums/AlarmDetailType.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.account.entity.enums; + +public enum AlarmDetailType { + COMMENT, POST, CHATROOM, MOIM, REVIEW, TODO, PLAN, EVENT +} diff --git a/src/main/java/com/dev/moim/domain/account/entity/enums/SNSType.java b/src/main/java/com/dev/moim/domain/account/entity/enums/AlarmType.java similarity index 56% rename from src/main/java/com/dev/moim/domain/account/entity/enums/SNSType.java rename to src/main/java/com/dev/moim/domain/account/entity/enums/AlarmType.java index 1ed17453..2a133e8b 100644 --- a/src/main/java/com/dev/moim/domain/account/entity/enums/SNSType.java +++ b/src/main/java/com/dev/moim/domain/account/entity/enums/AlarmType.java @@ -1,5 +1,5 @@ package com.dev.moim.domain.account.entity.enums; -public enum SNSType { - KAKAO, NAVER +public enum AlarmType { + PUSH, EVENT } diff --git a/src/main/java/com/dev/moim/domain/account/entity/enums/Gender.java b/src/main/java/com/dev/moim/domain/account/entity/enums/Gender.java index 3ce672c0..a06b10b3 100644 --- a/src/main/java/com/dev/moim/domain/account/entity/enums/Gender.java +++ b/src/main/java/com/dev/moim/domain/account/entity/enums/Gender.java @@ -1,5 +1,23 @@ package com.dev.moim.domain.account.entity.enums; +import com.dev.moim.global.error.GeneralException; +import com.fasterxml.jackson.annotation.JsonCreator; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_GENDER; + public enum Gender { - MALE, FEMALE + MALE, FEMALE; + + @JsonCreator + public static Gender from(String gender) { + if (gender == null || gender.isEmpty()) { + return null; + } + + try { + return Gender.valueOf(gender.toUpperCase()); + } catch (GeneralException e) { + throw new GeneralException(INVALID_GENDER); + } + } } diff --git a/src/main/java/com/dev/moim/domain/account/entity/enums/ProfileType.java b/src/main/java/com/dev/moim/domain/account/entity/enums/ProfileType.java new file mode 100644 index 00000000..b0fd0993 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/entity/enums/ProfileType.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.account.entity.enums; + +public enum ProfileType { + MAIN, SUB +} diff --git a/src/main/java/com/dev/moim/domain/account/entity/enums/Provider.java b/src/main/java/com/dev/moim/domain/account/entity/enums/Provider.java index a506ca62..6b06de0a 100644 --- a/src/main/java/com/dev/moim/domain/account/entity/enums/Provider.java +++ b/src/main/java/com/dev/moim/domain/account/entity/enums/Provider.java @@ -1,5 +1,5 @@ package com.dev.moim.domain.account.entity.enums; public enum Provider { - KAKAO, NAVER, GOOGLE, LOCAL -} + KAKAO, NAVER, GOOGLE, APPLE, LOCAL, UNREGISTERED +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/account/entity/enums/UserRole.java b/src/main/java/com/dev/moim/domain/account/entity/enums/UserRole.java new file mode 100644 index 00000000..a01148e3 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/entity/enums/UserRole.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.account.entity.enums; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum UserRole { + ROLE_USER("유저"), + ROLE_ADMIN("관리자"); + + private final String description; +} diff --git a/src/main/java/com/dev/moim/domain/account/repository/AlarmRepository.java b/src/main/java/com/dev/moim/domain/account/repository/AlarmRepository.java new file mode 100644 index 00000000..2789de04 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/repository/AlarmRepository.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.account.repository; + +import com.dev.moim.domain.account.entity.Alarm; +import com.dev.moim.domain.account.entity.User; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface AlarmRepository extends JpaRepository { + @Modifying + @Query("delete from Alarm a where a.user = :user") + void deleteAllByUser(User user); + + Slice findByUserAndIdLessThanOrderByIdDesc(User user, Long cursor, PageRequest of); +} diff --git a/src/main/java/com/dev/moim/domain/account/repository/CustomUserRepository.java b/src/main/java/com/dev/moim/domain/account/repository/CustomUserRepository.java new file mode 100644 index 00000000..66d43044 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/repository/CustomUserRepository.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.account.repository; + + +import com.dev.moim.domain.account.entity.User; + + +import java.util.List; + +public interface CustomUserRepository { + List getUserIdByChatRoomId(Long chatRoomId); + + List getUserByChatRoomId( + Long chatRoomId + ); +} diff --git a/src/main/java/com/dev/moim/domain/account/repository/UserProfileRepository.java b/src/main/java/com/dev/moim/domain/account/repository/UserProfileRepository.java new file mode 100644 index 00000000..580ed86c --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/repository/UserProfileRepository.java @@ -0,0 +1,32 @@ +package com.dev.moim.domain.account.repository; + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface UserProfileRepository extends JpaRepository { + + Optional findByUserIdAndProfileType(Long userId, ProfileType profileType); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO(up, um) from UserMoim um join um.userProfile up where um.moim = :moim and um.joinStatus = :joinStatus") + List findRandomProfile(Moim moim, JoinStatus joinStatus, Pageable pageable); + + @Query("SELECT up FROM UserProfile up " + + "WHERE up.user.id = :userId " + + "AND up.id > :cursor " + + "ORDER BY up.id ASC") + Slice findAllByUserIdAndCursor( + @Param("userId") Long userId, + @Param("cursor") Long cursor, + Pageable pageable); +} diff --git a/src/main/java/com/dev/moim/domain/account/repository/UserRepository.java b/src/main/java/com/dev/moim/domain/account/repository/UserRepository.java new file mode 100644 index 00000000..1827a29a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/repository/UserRepository.java @@ -0,0 +1,83 @@ +package com.dev.moim.domain.account.repository; + +import com.dev.moim.domain.account.entity.Alarm; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.Provider; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface UserRepository extends JpaRepository { + Optional findByEmailAndProvider(String email, Provider provider); + + Optional findByProviderIdAndProvider(String providerId, Provider provider); + + boolean existsByEmail(String email); + + boolean existsByProviderAndProviderId(Provider provider, String providerId); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO(up, um) " + + "from UserMoim um " + + "join um.userProfile up " + + "where um.moim.id = :moimId " + + "and um.joinStatus = :joinStatus " + + "and um.id > :cursor " + + "and up.name like %:searching% " + + "order by um.id") + Slice findUserByMoimId(Long moimId, String searching, JoinStatus joinStatus, Long cursor, Pageable pageable); + + @Query("select u from UserMoim um join um.user u where um.moim = :moim and um.joinStatus = :joinStatus") + List findUserByMoim(Moim moim, JoinStatus joinStatus); + + @Query("select u from UserMoim um join um.user u where um.moim = :moim and um.moimRole = :moimRole") + Optional findByMoimAndMoimCategory(Moim moim, MoimRole moimRole); + + @Query("select a from Alarm a join a.user u where u.lastAlarmTime < a.createdAt and u = :user") + List findAlarmByUser(User user); + + @Modifying + @Query("update User u set u.deviceId = null where u = :user") + void updateFcmTokenByUser(User user); + @Query("select rp.user.id from ReadPost rp where rp.post = :post and rp.isRead = false") + Set findReadUserId(Post post); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO(up, um) from UserMoim um join um.userProfile up where um.user.id in :userIds and um.moim = :moim and um.joinStatus = 'COMPLETE'") + List findReadUsersProfileByUsersId(Set userIds, Moim moim); + + @Query("select um from UserMoim um where um.moim = :moim and um.moimRole = 'OWNER' and um.joinStatus = 'COMPLETE'") + Optional findOwnerByMoim(Moim moim); + + @Query("select um.user from UserMoim um where um.moim = :moim and (um.moimRole = 'ADMIN' or um.moimRole = 'OWNER')") + List findAdmins(Moim moim); + + @Modifying + @Query("update User u set u.lastAlarmTime = :lastReadTime where u = :user") + void updateLastReadTime(User user, LocalDateTime lastReadTime); + + List findAllByIsPushAlarmTrueAndDeviceIdNotNull(); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO(up, um) " + + "from UserMoim um " + + "join um.userProfile up " + + "where um.moim.id = :moimId " + + "and um.joinStatus = :joinStatus " + + "and um.moimRole <> 'OWNER' " + + "and um.id > :cursor " + + "and up.name like %:searching% " + + "order by um.id") + Slice findUserByMoimIdExcludeOwner(Long moimId, String searching, JoinStatus joinStatus, Long cursor, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/account/repository/UserReviewRepository.java b/src/main/java/com/dev/moim/domain/account/repository/UserReviewRepository.java new file mode 100644 index 00000000..0ea6d436 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/repository/UserReviewRepository.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.account.repository; + +import com.dev.moim.domain.account.entity.UserReview; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface UserReviewRepository extends JpaRepository { + + Page findByUserId(Long userId, Pageable pageRequest); + + List findAllByUserId(Long userId); +} diff --git a/src/main/java/com/dev/moim/domain/account/service/AlarmCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/account/service/AlarmCommandServiceImpl.java new file mode 100644 index 00000000..dd4bdf6f --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/service/AlarmCommandServiceImpl.java @@ -0,0 +1,36 @@ +package com.dev.moim.domain.account.service; + +import com.dev.moim.domain.account.entity.Alarm; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.repository.AlarmRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class AlarmCommandServiceImpl implements AlarmService { + + private final AlarmRepository alarmRepository; + + @Override + public void saveAlarm(User sender, User receiver, String title, String content, AlarmType type, AlarmDetailType alarmDetailType, Long moimId, Long postId, Long commentId) { + + Alarm alarm = Alarm.builder() + .user(receiver) + .content(content) + .title(title) + .writerId(sender.getId()) + .alarmDetailType(alarmDetailType) + .moimId(moimId) + .postId(postId) + .commentId(commentId) + .alarmType(type) + .build(); + + alarmRepository.save(alarm); + } +} diff --git a/src/main/java/com/dev/moim/domain/account/service/AlarmService.java b/src/main/java/com/dev/moim/domain/account/service/AlarmService.java new file mode 100644 index 00000000..97883792 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/service/AlarmService.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.account.service; + + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; + +public interface AlarmService { + void saveAlarm(User sender, User receiver, String title, String content, AlarmType alarmType, AlarmDetailType alarmDetailType , Long moimId, Long postId, Long commentId); + +} diff --git a/src/main/java/com/dev/moim/domain/account/service/AuthService.java b/src/main/java/com/dev/moim/domain/account/service/AuthService.java new file mode 100644 index 00000000..c99d1cfb --- /dev/null +++ b/src/main/java/com/dev/moim/domain/account/service/AuthService.java @@ -0,0 +1,178 @@ +package com.dev.moim.domain.account.service; + +import com.dev.moim.domain.account.dto.*; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.global.email.EmailUtil; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.error.handler.EmailException; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.security.event.CustomAuthenticationSuccessEvent; +import com.dev.moim.global.security.principal.PrincipalDetails; +import com.dev.moim.global.security.util.JwtUtil; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import static com.dev.moim.domain.account.entity.enums.UserRole.ROLE_USER; +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class AuthService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final EmailUtil emailUtil; + + @Transactional + public TokenResponse join(JoinRequest request) { + + String encodedPassword = (request.provider() == Provider.LOCAL && request.password() != null) + ? passwordEncoder.encode(request.password()) + : null; + + User user = User.builder() + .provider(request.provider()) + .providerId(request.providerId()) + .deviceId(request.fcmToken()) + .email(request.email()) + .password(encodedPassword) + .userRole(ROLE_USER) + .lastAlarmTime(LocalDateTime.now()) + .userProfileList(new ArrayList<>()) + .gender(request.gender()) + .birth(request.birth()) + .residence(request.residence()) + .build(); + + UserProfile userProfile = UserProfile.builder() + .name(request.nickname()) + .profileType(ProfileType.MAIN) + .build(); + + userProfile.addUser(user); + + User newUser = userRepository.save(user); + + PrincipalDetails principalDetails = new PrincipalDetails(newUser); + + String accessToken = jwtUtil.createAccessToken(principalDetails); + String refreshToken = jwtUtil.createRefreshToken(principalDetails); + + try { + redisUtil.setValue(principalDetails.user().getId().toString(), refreshToken, jwtUtil.getRefreshTokenValiditySec()); + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } + + return new TokenResponse(accessToken, refreshToken, request.provider()); + } + + @Transactional + public TokenResponse reissueToken(String refreshToken) { + try { + if (!jwtUtil.isTokenValid(refreshToken)) { + throw new AuthException(AUTH_INVALID_TOKEN); + } + + String userId = jwtUtil.getUserId(refreshToken); + redisUtil.deleteValue(userId); + + User user = userRepository.findById(Long.valueOf(userId)) + .orElseThrow((() -> new AuthException(USER_NOT_FOUND))); + PrincipalDetails principalDetails = new PrincipalDetails(user); + + String newAccess = jwtUtil.createAccessToken(principalDetails); + String newRefresh = jwtUtil.createRefreshToken(principalDetails); + + redisUtil.setValue(userId, newRefresh, jwtUtil.getRefreshTokenValiditySec()); + + return new TokenResponse(newAccess, newRefresh, principalDetails.getProvider()); + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } catch (IllegalArgumentException e) { + throw new AuthException(AUTH_INVALID_TOKEN); + } catch (ExpiredJwtException e) { + throw new AuthException(AUTH_EXPIRED_TOKEN); + } + } + + public EmailVerificationCodeDTO sendCode(EmailDTO request) { + try { + String code = emailUtil.sendAuthorizationCodeEmail(request.email()); + return new EmailVerificationCodeDTO(request.email(), code); + } catch (Exception e) { + throw new EmailException(EMAIL_SEND_FAIL); + } + } + + public EmailVerificationResultDTO verifyCode(EmailVerificationCodeDTO request) { + String redisCode = redisUtil.getValue(request.code()); + if (redisCode == null) { + throw new EmailException(EMAIL_CODE_NOT_FOUND); + } + + boolean isCodeValid = request.code().equals(redisCode); + if (isCodeValid) { + redisUtil.deleteValue(request.email()); + } else { + throw new EmailException(INCORRECT_EMAIL_CODE); + } + + return new EmailVerificationResultDTO(request.email(), isCodeValid); + } + + @Transactional + public void quit(User user) { + try { + redisUtil.deleteValue(user.getId().toString()); + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } + userRepository.delete(user); + } + + @Transactional + public void updatePassword(UpdatePasswordDTO request) { + User user = userRepository.findByEmailAndProvider(request.email(), Provider.LOCAL) + .orElseThrow(() -> new AuthException(USER_NOT_FOUND)); + + user.updatePassword(passwordEncoder.encode(request.password())); + } + + @Transactional + @EventListener + public void handleAuthenticationSuccess(CustomAuthenticationSuccessEvent event) { + User user = event.getPrincipalDetails().user(); + String fcmToken = event.getFcmToken(); + + if (fcmToken != null && !fcmToken.isEmpty()) { + user.updateDeviceId(fcmToken); + userRepository.save(user); + } + } + + public void submitInquiry(User user, InquiryDTO request) { + try { + emailUtil.sendInquiryEmail(user, request.replyEmail(), request.content()); + } catch (Exception e) { + throw new EmailException(EMAIL_SEND_FAIL); + } + } +} diff --git a/src/main/java/com/dev/moim/domain/chatting/entity/Chatting.java b/src/main/java/com/dev/moim/domain/chatting/entity/Chatting.java deleted file mode 100644 index 780a659e..00000000 --- a/src/main/java/com/dev/moim/domain/chatting/entity/Chatting.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.dev.moim.domain.chatting.entity; - -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Chatting extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String chat; - - private Boolean isAnonymous; - - private String photoUrl; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_chatting_room_id") - private UserChattingRoom userChattingRoom; -} diff --git a/src/main/java/com/dev/moim/domain/chatting/entity/ChattingRoom.java b/src/main/java/com/dev/moim/domain/chatting/entity/ChattingRoom.java deleted file mode 100644 index 2daf6a53..00000000 --- a/src/main/java/com/dev/moim/domain/chatting/entity/ChattingRoom.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.dev.moim.domain.chatting.entity; - -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "chatting_room") -public class ChattingRoom extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "chatting_room_id") - private Long id; - - private String title; - - @OneToMany(mappedBy = "chattingRoom") - private List userChattingRoomList = new ArrayList<>(); -} diff --git a/src/main/java/com/dev/moim/domain/chatting/entity/UserChattingRoom.java b/src/main/java/com/dev/moim/domain/chatting/entity/UserChattingRoom.java deleted file mode 100644 index 217b72fc..00000000 --- a/src/main/java/com/dev/moim/domain/chatting/entity/UserChattingRoom.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.dev.moim.domain.chatting.entity; - -import com.dev.moim.domain.account.entity.User; -import com.dev.moim.domain.chatting.entity.enums.AlarmStatus; -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "user_chatting_room") -public class UserChattingRoom extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_chatting_room_id") - private Long id; - - @Enumerated(EnumType.STRING) - private AlarmStatus alarmStatus; - - @Column(name = "last_chatting_id") - private Long lastChattingId; - - @OneToMany(mappedBy = "userChattingRoom") - private List chattingList = new ArrayList<>(); - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "chatting_room_id") - private ChattingRoom chattingRoom; -} diff --git a/src/main/java/com/dev/moim/domain/chatting/entity/enums/AlarmStatus.java b/src/main/java/com/dev/moim/domain/chatting/entity/enums/AlarmStatus.java deleted file mode 100644 index ee1f3b13..00000000 --- a/src/main/java/com/dev/moim/domain/chatting/entity/enums/AlarmStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.dev.moim.domain.chatting.entity.enums; - -public enum AlarmStatus { - ON, OFF -} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/MoimCalendarController.java b/src/main/java/com/dev/moim/domain/moim/controller/MoimCalendarController.java new file mode 100644 index 00000000..c6ee1bea --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/MoimCalendarController.java @@ -0,0 +1,183 @@ +package com.dev.moim.domain.moim.controller; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.calender.*; +import com.dev.moim.domain.moim.service.CalenderCommandService; +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.security.annotation.AuthUser; +import com.dev.moim.global.validation.annotation.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/moim") +@Tag(name = "모임 캘린더 관련 컨트롤러") +public class MoimCalendarController { + + private final CalenderCommandService calenderCommandService; + private final CalenderQueryService calenderQueryService; + + @Operation(summary = "모임 (월) 일정 조회", description = "달력에서 특정 연도, 월에 등록되어 있는 일정들을 조회합니다. 모임에 참여하는 멤버만 조회 가능 합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다.") + }) + @GetMapping("/{moimId}/calender") + public BaseResponse> getMoimPlans( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month + ) { + return BaseResponse.onSuccess(calenderQueryService.getMoimPlans(user, moimId, year, month)); + } + + @Operation(summary = "모임 일정 생성", description = "모임의 새로운 일정을 추가합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다.") + }) + @PostMapping("/{moimId}/calender") + public BaseResponse createPlan( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @Valid @RequestBody PlanCreateDTO request + ) { + return BaseResponse.onSuccess(calenderCommandService.createPlan(user, moimId, request)); + } + + @Operation(summary = "모임 특정 일정 세부사항 조회", description = "특정 일정의 상세 내용과 일정 스케줄을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다.") + }) + @GetMapping("/{moimId}/plan/{planId}") + public BaseResponse getPlanDetails( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @PlanValidation @PathVariable Long planId + ) { + return BaseResponse.onSuccess(calenderQueryService.getPlanDetails(user, planId)); + } + + @Operation(summary = "모임 일정 스케줄 리스트 조회", description = "특정 일정의 스케줄들을 조회할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다.") + }) + @GetMapping("/{moimId}/plan/{planId}/schedules") + public BaseResponse getSchedules( + @UserMoimValidaton @PathVariable Long moimId, + @PlanValidation @PathVariable Long planId + ) { + return BaseResponse.onSuccess(calenderQueryService.getSchedules(moimId, planId)); + } + + @Operation(summary = "모임 일정 신청자 리스트 조회", description = "일정에 참가 신청한 사용자들을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다."), + @ApiResponse(responseCode = "PAGE_003", description = "page 값이 유효하지 않습니다."), + @ApiResponse(responseCode = "PAGE_004", description = "size 값이 유효하지 않습니다.") + }) + @GetMapping("/{moimId}/plan/{planId}/participants") + public BaseResponse getPlanParticipants( + @UserMoimValidaton @PathVariable Long moimId, + @PlanValidation @PathVariable Long planId, + @CheckPageValidation @RequestParam(name = "page") int page, + @CheckSizeValidation @RequestParam(name = "size") int size + ) { + return BaseResponse.onSuccess(calenderQueryService.getPlanParticipants(moimId, planId, page, size)); + } + + @Operation(summary = "모임 일정 수정", description = "일정 작성자 유저, 모임장 유저만 일정을 수정할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다."), + @ApiResponse(responseCode = "PLAN_006", description = "모임 일정 수정, 삭제 권한이 없는 유저입니다.") + }) + @PutMapping("/{moimId}/plan/{planId}") + public BaseResponse updatePlan( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @PlanAuthorityValidation @PathVariable Long planId, + @RequestBody PlanCreateDTO request + ) { + calenderCommandService.updatePlan(user, moimId, planId, request); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "모임 일정 삭제", description = "일정 작성자 유저, 일정을 삭제할 수 있습니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다."), + @ApiResponse(responseCode = "PLAN_006", description = "모임 일정 수정, 삭제 권한이 없는 유저입니다.") + }) + @DeleteMapping("/{moimId}/plan/{planId}") + public BaseResponse deletePlan( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @PlanAuthorityValidation @PathVariable Long planId + ) { + calenderCommandService.deletePlan(user, moimId, planId); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "모임 일정 참여 신청", description = "모임 멤버가 특정 일정에 신청하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다."), + @ApiResponse(responseCode = "PLAN_003", description = "이미 해당 모임 일정에 참여 신청했습니다.") + }) + @PostMapping("/{moimId}/plan/{planId}/participate") + public BaseResponse joinPlan( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @UserPlanDuplicateValidation @PathVariable Long planId + ) { + return BaseResponse.onSuccess(calenderCommandService.joinPlan(user, moimId, planId)); + } + + @Operation(summary = "모임 일정 참여 신청 취소", description = "모임 멤버가 모임 일정 신청을 취소하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "PLAN_004", description = "해당 일정에 참여 신청하지 않았습니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다.") + }) + @DeleteMapping("/{moimId}/plan/{planId}/participate") + public BaseResponse cancelPlanParticipation( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @UserPlanValidation @PathVariable Long planId + ) { + calenderCommandService.cancelPlanParticipation(user, moimId, planId); + return BaseResponse.onSuccess(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/moim/controller/MoimController.java b/src/main/java/com/dev/moim/domain/moim/controller/MoimController.java new file mode 100644 index 00000000..11d5f869 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/MoimController.java @@ -0,0 +1,292 @@ +package com.dev.moim.domain.moim.controller; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.controller.enums.MoimRequestJoin; +import com.dev.moim.domain.moim.controller.enums.MoimRequestRole; +import com.dev.moim.domain.moim.controller.enums.MoimRequestType; +import com.dev.moim.domain.moim.dto.*; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.service.MoimCommandService; +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.domain.user.dto.UserPreviewListDTO; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.security.annotation.AuthUser; +import com.dev.moim.global.validation.annotation.*; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1") +@Tag(name = "모임 관련 컨트롤러") +@RequiredArgsConstructor +@Validated +public class MoimController { + + private final MoimQueryService moimQueryService; + private final MoimCommandService moimCommandService; + + + // 홈 (모집 중인 모임 + 소개 하는 모임) + @Operation(summary = "인기 모임 조회 API", description = "인기 있는 모임을 조회 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/popular") + public BaseResponse getPopularMoim() { + MoimPreviewListDTO moimPreviewListDTO = moimQueryService.getPopularMoim(); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "신규 모임 조회 API", description = "신규 모임을 조회 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/new") + public BaseResponse getNewMoim(@RequestParam(name = "cursor") Long cursor, @RequestParam(name = "take") Integer take) { + MoimPreviewListDTO moimPreviewListDTO = moimQueryService.getNewMoim(cursor, take); + return BaseResponse.onSuccess(moimPreviewListDTO); + } + + // 내 모임 + @Operation(summary = "내가 활동 중인 모임 확인 API", description = "내가 활동 중인 모임을 확인 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/me") + public BaseResponse getMyMoim(@AuthUser User user, @CheckCursorValidation Long cursor, @CheckTakeValidation Integer take, @RequestParam(name = "moimRequestRole") MoimRequestRole moimRequestRole) { + MoimPreviewListDTO moimPreviewListDTO = moimQueryService.getUserMoim(user.getId(), cursor, take, moimRequestRole); + return BaseResponse.onSuccess(moimPreviewListDTO); + } + + // 다른 멤버 모임 + @Operation(summary = "다른 멤버가 활동 중인 모임 확인 API", description = "다른 멤버가 활동 중인 모임을 확인 합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/list/{userId}") + public BaseResponse getUserMoim( + @ExistUserValidation @PathVariable Long userId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take, + @RequestParam(name = "moimRequestRole") MoimRequestRole moimRequestRole + ) { + MoimPreviewListDTO moimPreviewListDTO = moimQueryService.getUserMoim(userId, cursor, take, moimRequestRole); + return BaseResponse.onSuccess(moimPreviewListDTO); + } + + // 모임 생성 + @Operation(summary = "모임 생성 API", description = "모임을 생성 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims") + public BaseResponse createMoim(@Parameter(hidden = true) @AuthUser User user, @RequestBody @Valid CreateMoimDTO createMoimDTO) { + Moim moim = moimCommandService.createMoim(user, createMoimDTO); + return BaseResponse.onSuccess(CreateMoimResultDTO.toCreateMoimResultDTO(moim)); + } + + // 모임 찾기 + @Operation(summary = "모임 찾기 API", description = "category와 검색어에 따라 모임을 찾습니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @Parameters({ + @Parameter(name = "moimRequestType", description = "선택한 카테 고리", example = "all"), + @Parameter(name = "name", description = "해당 이름이 포함된 모임을 찾습니다.", example = "고양이 모임") + }) + @GetMapping("/moims") + public BaseResponse findMoims( + @RequestParam(name = "moimRequestType", required = false) List moimRequestTypes, + @RequestParam(name = "name", required = false) String name, + @RequestParam(name = "cursor") @CheckCursorValidation Long cursor, + @RequestParam(name = "take") @CheckTakeValidation Integer take + ) { + MoimPreviewListDTO moimPreviewListDTO = moimQueryService.findMoims(moimRequestTypes, name, cursor, take); + return BaseResponse.onSuccess(moimPreviewListDTO); + } + + @Operation(summary = "모임 가입 신청 확인하기 (신청자 기준) API", description = "모임 가입 신청 확인하기 (신청자 기준) _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/my-requests") + public BaseResponse findMyRequestMoims( + @AuthUser User user, + @RequestParam(name = "cursor") @CheckCursorValidation Long cursor, + @RequestParam(name = "take") @CheckTakeValidation Integer take, + @RequestParam(name = "moimRequestJoin") MoimRequestJoin moimRequestJoin + ) { + MoimJoinRequestListDTO moimJoinRequestListDTO = moimQueryService.findMyRequestMoims(user, cursor, take, moimRequestJoin); + return BaseResponse.onSuccess(moimJoinRequestListDTO); + } + + + // 모임 스페 이스 api 나누기 + @Operation(summary = "모임 스페이스 정보 API", description = "모임 카테고리, 인원수, 성별, 설명 등을 리턴합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}") + public BaseResponse getMoimDetail(@AuthUser User user, @PathVariable Long moimId) { + MoimDetailDTO moimDetailDTO = moimQueryService.getMoimDetail(user, moimId); + return BaseResponse.onSuccess(moimDetailDTO); + } + + @Operation(summary = "모임 소개 영상 불러오기 API", description = "모임 소개 영상 보기. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/introduce-videos") + public BaseResponse getIntroduce(@PathVariable Long moimId) { + MoimIntroduceDTO moimIntroduceDTO = moimQueryService.getIntroduce(moimId); + return BaseResponse.onSuccess(moimIntroduceDTO); + } + + @Operation(summary = "모임 멤버 API", description = "모임에 참여한 멤버들을 조회합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/members") + public BaseResponse getMoimMembers(@PathVariable @UserMoimValidaton Long moimId, @RequestParam(name = "cursor") Long cursor, @RequestParam(name = "take") Integer take, @RequestParam(name = "search") String search) { + UserPreviewListDTO userPreviewListDTO = moimQueryService.getMoimMembers(moimId, cursor, take, search); + return BaseResponse.onSuccess(userPreviewListDTO); + } + + @Operation(summary = "모임 멤버 API (모임장 제외)", description = "모임장을 제외한 모임 멤버들을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/members/owner") + public BaseResponse getMoimMembersExcludeOwner( + @AuthUser User user, + @CheckOwnerValidation @UserMoimValidaton @PathVariable Long moimId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take, + @RequestParam(name = "search") String search) { + UserPreviewListDTO userPreviewListDTO = moimQueryService.getMoimMembersExcludeOwner(moimId, cursor, take, search); + return BaseResponse.onSuccess(userPreviewListDTO); + } + + @Operation(summary = "모임 탈퇴 하기 API", description = "모임을 탈퇴 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping ("/moims/withdraw") + public BaseResponse withDrawMoim(@AuthUser User user, @RequestBody @Valid WithMoimDTO withMoimDTO) { + moimCommandService.withDrawMoim(user, withMoimDTO); + return BaseResponse.onSuccess("탈퇴 신청하였습니다."); + } + + @Operation(summary = "모임 가입 신청 상태 확인하기 (초기값 0으로 해주세요) API", description = "모임 가입 신청한 멤버들을 확인합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping ("/moims/{moimId}/requests/users") + public BaseResponse findRequestMember(@AuthUser User user, @PathVariable @UserMoimValidaton Long moimId, @RequestParam(name = "cursor") Long cursor, @RequestParam(name = "take") Integer take, @RequestParam(name = "search") String search) { + UserPreviewListDTO userPreviewListDTO = moimQueryService.findRequestMember(user, moimId, cursor, take, search); + return BaseResponse.onSuccess(userPreviewListDTO); + } + + @Operation(summary = "모임 수정 API", description = "모임을 수정 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PutMapping("/moims") + public BaseResponse modifyMoimInfo(@RequestBody @Valid UpdateMoimDTO updateMoimDTO) { + moimCommandService.modifyMoimInfo(updateMoimDTO); + return BaseResponse.onSuccess("모임 수정에 성공하였습니다."); + } + + @Operation(summary = "모임 신청 API", description = "모임을 신청 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/{moimId}/requests") + public BaseResponse joinMoim(@AuthUser User user, @PathVariable Long moimId) { + moimCommandService.joinMoim(user, moimId); + return BaseResponse.onSuccess("모임 가입에 신청에 성공하였습니다."); + } + + @Operation(summary = "모임 가입 신청 받아주기 API", description = "모임 가입 신청 받아줍니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/{moimId}/accept") + public BaseResponse acceptMoim(@AuthUser User user, @RequestBody @Valid MoimJoinConfirmRequestDTO moimJoinConfirmRequestDTO) { + moimCommandService.acceptMoim(user, moimJoinConfirmRequestDTO); + return BaseResponse.onSuccess("모임 가입에 받아주기에 성공하였습니다."); + } + + @Operation(summary = "멤버 권한 수정하기 API", description = "멤버 권한을 수정합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PatchMapping("/moims/{moimId}/authorities") + public BaseResponse changeMemberAuthorities(@AuthUser User user, @RequestBody @Valid ChangeAuthorityRequestDTO changeAuthorityRequestDTO) { + ChangeAuthorityResponseDTO changeAuthorityResponseDTO = moimCommandService.changeMemberAuthorities(user, changeAuthorityRequestDTO); + return BaseResponse.onSuccess(changeAuthorityResponseDTO); + } + + @Operation(summary = "가입 거절하기 API", description = "가입을 거절합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/users/reject") + public BaseResponse rejectMoims(@RequestBody @Valid MoimJoinConfirmRequestDTO moimJoinConfirmRequestDTO) { + moimCommandService.rejectMoims(moimJoinConfirmRequestDTO); + return BaseResponse.onSuccess("거절에 성공하였습니다."); + } + + @Operation(summary = "모임장 위임하기 API", description = "모임장을 다른 사람에게 위임합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/users/leader-change") + public BaseResponse changeMoimLeader(@AuthUser User user, @RequestBody @Valid ChangeMoimLeaderRequestDTO changeMoimLeaderRequestDTO) { + moimCommandService.changeMoimLeader(user, changeMoimLeaderRequestDTO); + return BaseResponse.onSuccess("모임장 위임에 성공하였습니다."); + } + + @Operation(summary = "모임 신청자 확인 API", description = "모임 신청 확인하는 것을 없앱니다.. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/{moimId}/my-requests/confirm") + public BaseResponse findMyRequestMoimsConfirm(@AuthUser User user, @PathVariable Long moimId) { + moimCommandService.findMyRequestMoimsConfirm(user, moimId); + return BaseResponse.onSuccess("모임 신청 확인하는 것을 없애는 데 성공 하였습니다."); + } + + @Operation(summary = "모임 탈퇴 시키기 API", description = "모임에서 탈퇴시킵니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @DeleteMapping("/users/{userId}/moims/{moimId}/expel") + public BaseResponse moimExpel(@AuthUser User user, @PathVariable Long userId, @PathVariable Long moimId) { + moimCommandService.moimExpel(user, userId, moimId); + return BaseResponse.onSuccess("모임 탈퇴 시키기에 성공하였습니다."); + } + + @Operation(summary = "자신의 role 반환 API", description = "자신의 role 반환합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/my-roles") + public BaseResponse moimsMyRole(@AuthUser User user, @PathVariable Long moimId) { + MoimRoleResponse moimRoleResponse = moimCommandService.moimsMyRole(user, moimId); + return BaseResponse.onSuccess(moimRoleResponse); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/MoimPostController.java b/src/main/java/com/dev/moim/domain/moim/controller/MoimPostController.java new file mode 100644 index 00000000..ca3c4873 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/MoimPostController.java @@ -0,0 +1,304 @@ +package com.dev.moim.domain.moim.controller; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.controller.enums.PostRequestType; +import com.dev.moim.domain.moim.dto.post.*; +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.PostBlock; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import com.dev.moim.domain.moim.service.PostCommandService; +import com.dev.moim.domain.moim.service.PostQueryService; +import com.dev.moim.domain.user.dto.UserPreviewDTO; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.security.annotation.AuthUser; +import com.dev.moim.global.validation.annotation.CheckCursorValidation; +import com.dev.moim.global.validation.annotation.CheckTakeValidation; +import com.dev.moim.global.validation.annotation.UserMoimValidaton; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1") +@Tag(name = "모임 게시글 관련 컨트롤러") +@RequiredArgsConstructor +@Validated +public class MoimPostController { + + private final PostQueryService postQueryService; + private final PostCommandService postCommandService; + private final UserQueryService userQueryService; + private final UserMoimRepository userMoimRepository; + + @Operation(summary = "모임 게시판 목록 API", description = "모임 게시판 목록을 조회 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/posts") + public BaseResponse getMoimPostList( + @AuthUser User user, + @PathVariable @UserMoimValidaton Long moimId, + @RequestParam(name = "postType") PostRequestType postRequestType, + @Parameter(description = "처음 값은 1로 해주 세요.") @RequestParam(name = "cursor") @CheckCursorValidation Long cursor, + @RequestParam(name = "take") @CheckTakeValidation Integer take + ) { + MoimPostPreviewListDTO moimPostPreviewListDTO = postQueryService.getMoimPostList(user, moimId, postRequestType, cursor, take); + return BaseResponse.onSuccess(moimPostPreviewListDTO); + } + + @Operation(summary = "모임 게시글 상세 API", description = "모임 게시글을 상세 보기합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/posts/{postId}") + public BaseResponse getMoimPost( + @AuthUser User user, + @PathVariable Long moimId, + @PathVariable Long postId + ) { + MoimPostDetailDTO postDetailDTO = postQueryService.getMoimPost(user, moimId, postId); + return BaseResponse.onSuccess(postDetailDTO); + } + + @Operation(summary = "모임 게시글 작성 API", description = "모임 게시글을 작성 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts") + public BaseResponse createMoimPost(@AuthUser User user, @RequestBody @Valid CreateMoimPostDTO createMoimPostDTO) { + Post post = postCommandService.createMoimPost(user, createMoimPostDTO); + return BaseResponse.onSuccess(CreateMoimPostResultDTO.toCreateMoimPostDTO(post)); + } + + @Operation(summary = "모임 게시글 신고 API", description = "모임 게시글을 신고 / 신고 취소 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/reports") + public BaseResponse reportMoimPost(@AuthUser User user, @RequestBody PostReportDTO postReportDTO) { + postCommandService.reportMoimPost(user, postReportDTO); + return BaseResponse.onSuccess("게시물 신고가 완료되었습니다."); + } + + @Operation(summary = "댓글 무한 스크롤 API", description = "댓글을 무한 스크롤로 조회합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/posts/{postId}/comments") + public BaseResponse getcomments( + @AuthUser User user, + @PathVariable Long moimId, + @PathVariable Long postId, + @Parameter(description = "처음 값은 0로 해주 세요.") @RequestParam(name = "cursor") Long cursor, + @RequestParam(name = "take") @CheckTakeValidation Integer take + ) { + CommentResponseListDTO commentResponseListDTO = postQueryService.getcomments(user, moimId, postId, cursor, take); + return BaseResponse.onSuccess(commentResponseListDTO); + } + + @Operation(summary = "모임 댓글 작성 API", description = "모임 댓글 작성 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/comments") + public BaseResponse createComment(@AuthUser User user, @RequestBody @Valid CreateCommentDTO createCommentDTO) { + Comment comment = postCommandService.createComment(user, createCommentDTO); + return BaseResponse.onSuccess(CreateCommentResultDTO.toCreateCommentResultDTO(comment)); + } + + @Operation(summary = "모임 대댓글 작성 API", description = "모임 대댓글 작성 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/comments/comments") + public BaseResponse createCommentComment(@AuthUser User user, @RequestBody @Valid CreateCommentCommentDTO createCommentCommentDTO) { + Comment comment = postCommandService.createCommentComment(user, createCommentCommentDTO); + return BaseResponse.onSuccess(CreateCommentResultDTO.toCreateCommentResultDTO(comment)); + } + + @Operation(summary = "모임 게시글 좋아요/좋아요 취소 API", description = "모임 게시글 좋아요가 되어있으면 취소를 아니면 좋아요 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/like") + public BaseResponse postLike(@AuthUser User user, @RequestBody PostLikeDTO postLikeDTO) { + postCommandService.postLike(user, postLikeDTO); + return BaseResponse.onSuccess(LikeResultDTO.toLikeResultDTO(postQueryService.isPostLike(user.getId(), postLikeDTO.postId()))); + } + + @Operation(summary = "모임 댓글 좋아요/좋아요 취소 API", description = "모임 댓글 좋아요가 되어있으면 취소를 아니면 좋아요 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/comments/Like") + public BaseResponse commentLike(@AuthUser User user, @RequestBody CommentLikeDTO commentLikeDTO) { + postCommandService.commentLike(user, commentLikeDTO); + return BaseResponse.onSuccess(LikeResultDTO.toLikeResultDTO(postQueryService.isCommentLike(user.getId(), commentLikeDTO.commentId()))); + } + + @Operation(summary = "모임 게시글 삭제 API", description = "모임 게시글을 삭제 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @DeleteMapping("/moims/posts/{postId}") + public BaseResponse deletePost(@AuthUser User user, @PathVariable Long postId) { + postCommandService.deletePost(user, postId); + return BaseResponse.onSuccess("게시글이 삭제 되었습니다."); + } + + @Operation(summary = "모임 게시글 수정 API", description = "모임 게시글을 수정 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PutMapping("/moims/posts") + public BaseResponse updatePost(@AuthUser User user, @RequestBody @Valid UpdateMoimPostDTO updateMoimPostDTO) { + Post post = postCommandService.updatePost(user, updateMoimPostDTO); + return BaseResponse.onSuccess(UpdatePostResponseDTO.toUpdatePostResponseDTO(post)); + } + + @Operation(summary = "모임 게시글 차단 API", description = "모임 게시글을 차단/차단해제 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/block") + public BaseResponse blockPost(@AuthUser User user, @RequestBody @Valid PostBlockDTO postBlockDTO) { + postCommandService.blockPost(user, postBlockDTO); + return BaseResponse.onSuccess("게시글이 차단 되었습니다."); + } + + @Operation(summary = "모임 댓글 삭제 API", description = "모임 댓글을 삭제 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/comments/{commentId}/delete") + public BaseResponse deleteComment(@AuthUser User user, @PathVariable Long commentId) { + postCommandService.deleteComment(user, commentId); + return BaseResponse.onSuccess("댓글이 삭제 되었습니다."); + } + + @Operation(summary = "모임 댓글 삭제 API", description = "모임 댓글을 삭제 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PutMapping("/moims/comments") + public BaseResponse updateComment(@AuthUser User user, @RequestBody @Valid CommentUpdateRequestDTO commentUpdateRequestDTO) { + postCommandService.updateComment(user, commentUpdateRequestDTO); + return BaseResponse.onSuccess("댓글이 수정 되었습니다."); + } + + @Operation(summary = "모임 댓글 신고 API", description = "모임 댓글을 신고 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/comments/reports") + public BaseResponse reportComment(@AuthUser User user, @RequestBody CommentReportDTO commentReportDTO) { + postCommandService.reportComment(user, commentReportDTO); + return BaseResponse.onSuccess("댓글이 신고 되었습니다."); + } + + @Operation(summary = "모임 댓글 차단 API", description = "모임 댓글을 차단 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/comments/block") + public BaseResponse blockComment(@AuthUser User user, @RequestBody CommentBlockDTO commentBlockDTO) { + postCommandService.blockComment(user, commentBlockDTO); + return BaseResponse.onSuccess("댓글이 차단 되었습니다."); + } + + @Operation(summary = "모임 소개 게시물 조회 API", description = "모임 소개 게시물 조회. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/global/posts") + public BaseResponse getIntroductionPosts( + @Parameter(description = "처음 값은 1로 해주 세요.") @RequestParam(name = "cursor") @CheckCursorValidation Long cursor, + @RequestParam(name = "take") @CheckTakeValidation Integer take) { + MoimPostPreviewListDTO moimPostPreviewListDTO = postQueryService.getIntroductionPosts(cursor, take); + return BaseResponse.onSuccess(moimPostPreviewListDTO); + } + + @Operation(summary = "모임 소개 게시물 상세 조회 API", description = "모임 소개 게시물 상세 조회. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/global/posts/{postId}") + public BaseResponse getIntroductionPost(@AuthUser User user, @PathVariable Long postId) { + Post post = postQueryService.getIntroductionPost( postId); + Boolean postLike = postQueryService.isPostLike(user.getId(), postId); + Optional userMoim = userMoimRepository.findByPost(post); + return BaseResponse.onSuccess(MoimPostDetailDTO.toMoimPostDetailDTO(post, postLike, userMoim)); + } + + @Operation(summary = "게시물 읽을 사람 (아직 안읽은사람) API", description = "아직 해당 공지사항을 안 읽은 사람을 리턴합니다.. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/{moimId}/posts/{postId}/users/un-read") + public BaseResponse> getUnReadUsers(@AuthUser User user, @PathVariable @UserMoimValidaton Long moimId, @PathVariable Long postId) { + List unReadUserListByPost = userQueryService.findUnReadUserListByPost(user, moimId, postId); + return BaseResponse.onSuccess(unReadUserListByPost); + } + + @Operation(summary = "가입된 모입 정보 최신순 API", description = "모임에는 무슨 일이 일어날까요?의 정보를 리턴합니다.. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/posts/what") + public BaseResponse> getPostsByJoinMoims(@AuthUser User user) { + List joinMoimPostsResponseDTOList = postQueryService.getPostsByJoinMoims(user); + return BaseResponse.onSuccess(joinMoimPostsResponseDTOList); + } + + @Operation(summary = "게시물 공지사항 생성 API", description = "공지 사항을 생성합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/announcement") + public BaseResponse createAnnouncement(@AuthUser User user, @RequestBody @Valid AnnouncementRequestDTO announcementRequestDTO) { + Long postId = postCommandService.createAnnouncement(user, announcementRequestDTO); + return BaseResponse.onSuccess(postId); + } + + @Operation(summary = "게시물 공지사항 읽음 표시하기 API", description = "공지 사항을 읽었음을 표시합니다.. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @PostMapping("/moims/posts/announcement/confirm") + public BaseResponse announcementConfirm(@AuthUser User user, @RequestBody @Valid AnnouncementConfirmRequestDTO announcementRequestDTO) { + postCommandService.announcementConfirm(user, announcementRequestDTO); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "모임 차단한 댓글 API", description = "모임 차단한 댓글을 조회 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/posts/comments/block") + public BaseResponse> findBlockComments(@AuthUser User user) { + List blockCommentResponseList = postQueryService.findBlockComments(user); + return BaseResponse.onSuccess(blockCommentResponseList); + } + + @Operation(summary = "모임 차단한 게시글 조회 API", description = "모임 차단한 게시글 조회 합니다. _by 제이미_") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + }) + @GetMapping("/moims/posts/block") + public BaseResponse> findBlockPosts(@AuthUser User user) { + List moimPostPreviewDTOList = postQueryService.findBlockPosts(user); + return BaseResponse.onSuccess(moimPostPreviewDTOList); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/MoimTodoController.java b/src/main/java/com/dev/moim/domain/moim/controller/MoimTodoController.java new file mode 100644 index 00000000..fe79102a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/MoimTodoController.java @@ -0,0 +1,268 @@ +package com.dev.moim.domain.moim.controller; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.todo.*; +import com.dev.moim.domain.moim.service.TodoCommandService; +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.security.annotation.AuthUser; +import com.dev.moim.global.validation.annotation.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +@Tag(name = "todo 관련 컨트롤러") +public class MoimTodoController { + + private final TodoCommandService todoCommandService; + private final TodoQueryService todoQueryService; + + @Operation(summary = "모임 todo 생성", description = "모임 관리자 회원이 모임의 멤버들에게 todo를 줄 수 있는 기능입니다. 관리자 회원만 가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "COMMON_002", description = "입력된 정보에 오류가 있습니다. 필드별 오류 메시지를 참조하세요."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_004", description = "Todo를 할당받을 유저를 지정하지 않았습니다."), + @ApiResponse(responseCode = "TODO_005", description = "전체 선택인 경우 특정 assignee를 지정할 수 없습니다.") + }) + @PostMapping("/moims/{moimId}/todos") + public BaseResponse createTodo( + @AuthUser User user, + @CheckAdminValidation @UserMoimValidaton @PathVariable Long moimId, + @Valid @RequestBody CreateTodoDTO request + ) { + return BaseResponse.onSuccess(todoCommandService.createTodo(user, moimId, request)); + } + + @Operation(summary = "todo 상세 조회 (할당된 유저)", description = "유저가 자신에게 할당된 특정 todo의 세부사항을 상세 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "TODO_002", description = "해당 유저에게 부여된 todo가 아닙니다.") + }) + @GetMapping("/moims/{moimId}/todos/{todoId}/for-me") + public BaseResponse getTodoDetailForAssignee( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @TodoAssigneeValidation @PathVariable Long todoId + ) { + return BaseResponse.onSuccess(todoQueryService.getTotalDetailForAssignee(user, todoId)); + } + + @Operation(summary = "todo 상세 조회 (모임 관리자)", description = "관리자 회원이 특정 모임의 특정 todo의 세부사항을 상세 조회합니다. ") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다.") + }) + @GetMapping("/moims/{moimId}/todos/{todoId}/admins/detail") + public BaseResponse getTodoDetailForAdmin( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @TodoValidation @PathVariable Long todoId + ) { + return BaseResponse.onSuccess(todoQueryService.getTodoDetailForAdmin(todoId)); + } + + @Operation(summary = "todo 할당받은 멤버 리스트 조회 (모임 관리자)", description = "관리자 회원이 특정 모임의 특정 todo를 할당받은 멤버 리스트를 조회합니다. ") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_012", description = "user moim을 찾을 수 없습니다.") + }) + @GetMapping("/moims/{moimId}/todos/{todoId}/admins/assignee-list") + public BaseResponse getTodoAssigneeListForAdmin( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @TodoValidation @PathVariable Long todoId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(todoQueryService.getTodoAssigneeListForAdmin(todoId, cursor, take)); + } + + @Operation(summary = "todo 할당받은 멤버 제외한 모임 멤버 조회 (모임 관리자)", description = "관리자 회원이 특정 모임의 특정 todo를 할당받은 멤버를 제외한 모임 멤버들을 조회합니다. ") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_012", description = "user moim을 찾을 수 없습니다.") + }) + @GetMapping("/moims/{moimId}/todos/{todoId}/admins/non-assignee-list") + public BaseResponse getTodoNonAssigneeListForAdmin( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @TodoValidation @PathVariable Long todoId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(todoQueryService.getTodoNonAssigneeListForAdmin(moimId, todoId, cursor, take)); + } + + @Operation(summary = "특정 모임의 todo 리스트 조회 (모임 관리자)", description = "관리자 회원이 특정 모임의 todo 리스트를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + }) + @GetMapping("/moims/{moimId}/todos/admins") + public BaseResponse getMoimTodoListForAdmin( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(todoQueryService.getMoimTodoListForAdmin(moimId, cursor, take)); + } + + @Operation(summary = "특정 모임 관리자 회원이 부여한 todo 리스트 조회 (모임 관리자)", description = "특정 관리자 회원이 특정 모임에서 자신이 부여한 todo 리스트를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + }) + @GetMapping("/moims/{moimId}/todos/by-me") + public BaseResponse getSpecificMoimTodoListByMe( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(todoQueryService.getSpecificMoimTodoListByMe(user, moimId, cursor, take)); + } + + @Operation(summary = "특정 모임에서 부여받은 todo 리스트 조회 (모임 멤버)", description = "특정 멤버가 특정 모임에서 자신이 부여받은 todo 리스트를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다.") + }) + @GetMapping("/moims/{moimId}/todos/for-assignee") + public BaseResponse getAssignedTodoListForUserInSpecificMoim( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(todoQueryService.getAssignedTodoListForUserInSpecificMoim(user, moimId, cursor, take)); + } + + @Operation(summary = "자신이 부여한 todo 리스트 조회", description = "회원이 자신이 부여한 todo 리스트를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/todos/by-me") + public BaseResponse getTodoListByMe( + @AuthUser User user, + @RequestParam(name = "cursor", required = false) Long cursor, + @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(todoQueryService.getTodoListByMe(user, cursor, take)); + } + + @Operation(summary = "부여된 todo 상태 업데이트", description = "회원이 자신에게 부여된 todo의 상태를 변경합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "COMMON_002", description = "입력된 정보에 오류가 있습니다. 필드별 오류 메시지를 참조하세요."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "TODO_002", description = "해당 유저에게 부여된 todo가 아닙니다."), + @ApiResponse(responseCode = "TODO_003", description = "업데이트 요청한 todo status가 기존 status와 동일합니다."), + @ApiResponse(responseCode = "TODO_006", description = "마감 기한이 지난 Todo입니다."), + @ApiResponse(responseCode = "TODO_007", description = "해당 todo status로 변경할 수 없습니다.") + }) + @PutMapping("/moims/{moimId}/todos/assignee/{todoId}") + public BaseResponse updateUserTodoStatus( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @TodoAssigneeValidation @PathVariable Long todoId, + @Valid @RequestBody UpdateTodoStatusDTO request + ) { + return BaseResponse.onSuccess(todoCommandService.updateUserTodoStatus(user, todoId, request)); + } + + @Operation(summary = "todo 수정", description = "모임 관리자 회원이 특정 todo 내용을 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "COMMON_002", description = "입력된 정보에 오류가 있습니다. 필드별 오류 메시지를 참조하세요."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "TODO_004", description = "Todo를 할당받을 유저를 지정하지 않았습니다."), + @ApiResponse(responseCode = "TODO_005", description = "전체 선택인 경우 특정 assignee를 지정할 수 없습니다.") + }) + @PutMapping("/moims/{moimId}/todos/admin/{todoId}") + public BaseResponse updateTodo( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @TodoValidation @PathVariable Long todoId, + @Valid @RequestBody UpdateTodoDTO request + ) { + todoCommandService.updateTodo(user, moimId, todoId, request); + return BaseResponse.onSuccess("todo 수정 성공했습니다."); + } + + @Operation(summary = "todo 삭제", description = "모임 관리자 회원이 특정 todo 내용을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다.") + }) + @DeleteMapping("/moims/{moimId}/todos/admin/{todoId}") + public BaseResponse deleteTodo( + @AuthUser User user, + @CheckAdminValidation @PathVariable Long moimId, + @TodoValidation @PathVariable Long todoId + ) { + todoCommandService.deleteTodo(todoId); + return BaseResponse.onSuccess("todo 삭제 성공했습니다."); + } + + @Operation(summary = "todo assignee 추가", description = "모임 관리자 회원이 특정 todo를 할당받을 모임 멤버를 추가합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "COMMON_002", description = "입력된 정보에 오류가 있습니다. 필드별 오류 메시지를 참조하세요."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "TODO_008", description = "이미 todo를 할당받은 멤버를 지정했습니다.") + }) + @PutMapping("/moims/todos/admin/assignees/new") + public BaseResponse addAssignees( + @AuthUser User user, + @Valid @RequestBody AddTodoAssigneeDTO request + ) { + todoCommandService.addAssignees(user, request); + return BaseResponse.onSuccess("todo assignee 추가 성공했습니다."); + } + + @Operation(summary = "todo assignee 삭제", description = "모임 관리자 회원이 특정 todo를 할당받을 모임 멤버를 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "COMMON_002", description = "입력된 정보에 오류가 있습니다. 필드별 오류 메시지를 참조하세요."), + @ApiResponse(responseCode = "MOIM_002", description = "모임 관리자 회원이 아닙니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "TODO_001", description = "Todo를 찾을 수 없습니다."), + @ApiResponse(responseCode = "TODO_002", description = "해당 유저에게 부여된 todo가 아닙니다.") + }) + @PutMapping("/moims/todos/admin/assignees/current") + public BaseResponse deleteAssignees( + @AuthUser User user, + @Valid @RequestBody DeleteTodoAssigneeDTO request + ) { + todoCommandService.deleteAssignees(request); + return BaseResponse.onSuccess("todo assignee 삭제 성공했습니다."); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestJoin.java b/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestJoin.java new file mode 100644 index 00000000..112a6a28 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestJoin.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.controller.enums; + +public enum MoimRequestJoin { + COMPLETE, LOADING, REJECT, ALL +} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestRole.java b/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestRole.java new file mode 100644 index 00000000..14eb5c1d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestRole.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.controller.enums; + +public enum MoimRequestRole { + OWNER, ADMIN, MEMBER, ALL +} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestType.java b/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestType.java new file mode 100644 index 00000000..c8effd67 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/enums/MoimRequestType.java @@ -0,0 +1,6 @@ +package com.dev.moim.domain.moim.controller.enums; + + +public enum MoimRequestType { + SPORTS, TECH, HUMANITY, LANGUAGE, ARTICLE, VOLUNTEER, RELIGION, PHOTO, ANIMAL, MUSIC, SELF, ETC +} diff --git a/src/main/java/com/dev/moim/domain/moim/controller/enums/PostRequestType.java b/src/main/java/com/dev/moim/domain/moim/controller/enums/PostRequestType.java new file mode 100644 index 00000000..da539439 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/controller/enums/PostRequestType.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.controller.enums; + +public enum PostRequestType { + ANNOUNCEMENT, REVIEW, WELCOME, FREE, ALL +} diff --git a/src/main/java/com/dev/moim/domain/moim/converter/PostConverter.java b/src/main/java/com/dev/moim/domain/moim/converter/PostConverter.java new file mode 100644 index 00000000..1ab2fe89 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/converter/PostConverter.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.moim.converter; + +import com.dev.moim.domain.moim.dto.post.CommentResponseDTO; +import com.dev.moim.domain.moim.dto.post.CommentResponseListDTO; +import com.dev.moim.domain.moim.dto.post.MoimPostPreviewDTO; +import com.dev.moim.domain.moim.dto.post.MoimPostPreviewListDTO; +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.service.PostQueryService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; + +import java.util.List; + +@RequiredArgsConstructor +public class PostConverter { + + private final PostQueryService postQueryService; + + public static MoimPostPreviewListDTO toMoimPostPreviewListDTO(List moimPostPreviewDTOList,Boolean hasNext, Long nextCursor) { + + return MoimPostPreviewListDTO.toMoimPostPreviewListDTO(moimPostPreviewDTOList, nextCursor, hasNext); + } + + +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/ChangeAuthorityRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/ChangeAuthorityRequestDTO.java new file mode 100644 index 00000000..a5736853 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/ChangeAuthorityRequestDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.global.validation.annotation.CheckAdminValidation; + +public record ChangeAuthorityRequestDTO( + @CheckAdminValidation + Long moimId, + MoimRole moimRole, + Long userId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/ChangeAuthorityResponseDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/ChangeAuthorityResponseDTO.java new file mode 100644 index 00000000..c0cd6aa6 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/ChangeAuthorityResponseDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.MoimRole; + +public record ChangeAuthorityResponseDTO( + Long userId, + MoimRole moimRole +) { + public static ChangeAuthorityResponseDTO toChangeAuthorityResponseDTO(Long userId, MoimRole moimRole) { + return new ChangeAuthorityResponseDTO(userId, moimRole); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/ChangeMoimLeaderRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/ChangeMoimLeaderRequestDTO.java new file mode 100644 index 00000000..dac2a647 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/ChangeMoimLeaderRequestDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.global.validation.annotation.CheckOwnerValidation; + +public record ChangeMoimLeaderRequestDTO( + Long userId, + @CheckOwnerValidation + Long moimId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/CreateMoimDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/CreateMoimDTO.java new file mode 100644 index 00000000..0914a77e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/CreateMoimDTO.java @@ -0,0 +1,22 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +public record CreateMoimDTO( + @Length(min = 1, max = 255) + String title, + @Length(min = 1, max = 255) + String location, + MoimCategory moimCategory, + String imageKeyName, + String introduceVideoKeyName, + @Length(min = 1, max = 255) + String introduceVideoTitle, + @Length(min = 1, max = 255) + String introduction +) { + +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/CreateMoimResultDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/CreateMoimResultDTO.java new file mode 100644 index 00000000..f77b94aa --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/CreateMoimResultDTO.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.Moim; + +import java.time.LocalDateTime; + +public record CreateMoimResultDTO( + Long moimId, + LocalDateTime createAt, + LocalDateTime updateAt +) { + public static CreateMoimResultDTO toCreateMoimResultDTO(Moim moim) { + return new CreateMoimResultDTO(moim.getId(), moim.getCreatedAt(), moim.getUpdatedAt()); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementDTO.java new file mode 100644 index 00000000..bb01f729 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto; + +import java.time.LocalDateTime; + +public record MoimAnnouncementDTO( + String title, + String content, + LocalDateTime createAt +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementListDTO.java new file mode 100644 index 00000000..14d993c0 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementListDTO.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.moim.dto; + +import java.util.List; + +public record MoimAnnouncementListDTO( + List moimAnnouncementDTOList +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementPreviewDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementPreviewDTO.java new file mode 100644 index 00000000..b2c0cf9c --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimAnnouncementPreviewDTO.java @@ -0,0 +1,9 @@ +package com.dev.moim.domain.moim.dto; + +public record MoimAnnouncementPreviewDTO( + Long announcementId, + String title, + String content, + String writer +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimCalendarDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimCalendarDTO.java new file mode 100644 index 00000000..20c4aab8 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimCalendarDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto; + +import java.time.LocalDate; + +public record MoimCalendarDTO( + Long moimCalendarId, + String title, + String cost, + String participant, + String address, + LocalDate date +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimCommentDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimCommentDTO.java new file mode 100644 index 00000000..31a95535 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimCommentDTO.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.Post; + +import java.time.LocalDateTime; + +public record MoimCommentDTO( + Long moimCommentId, + String content, + String writer, + Integer likeCount, + LocalDateTime updateAt, + LocalDateTime createAt +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimDetailDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimDetailDTO.java new file mode 100644 index 00000000..7b883291 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimDetailDTO.java @@ -0,0 +1,55 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO; +import com.dev.moim.domain.user.dto.UserPreviewDTO; + +import java.time.LocalDateTime; +import java.util.List; + +public record MoimDetailDTO( + Long moimId, + JoinStatus joinStatus, + MoimRole myMoimRole, + String title, + String description, + String profileImageUrl, + MoimCategory category, + Double averageAge, + int diaryCount, + int moimReviewCount, + Long maleCount, + Long femaleCount, + Long nonSelectCount, + int memberCount, + String address, + LocalDateTime createAt, + LocalDateTime updateAt, + List userPreviewDTOList +) { + public static MoimDetailDTO toMoimDetailDTO(Moim moim, MoimRole myMoimRole,JoinStatus joinStatus, String profileImageUrl, Double averageAge, int diaryCount, int moimReviewCount, Long maleCount, Long femaleCount, Long nonSelectCount, int memberCount, List userPreviewDTOList) { + return new MoimDetailDTO( + moim.getId(), + joinStatus, + myMoimRole, + moim.getName(), + moim.getIntroduction(), + profileImageUrl, + moim.getMoimCategory(), + averageAge, + diaryCount, + moimReviewCount, + maleCount, + femaleCount, + nonSelectCount, + memberCount, + moim.getLocation(), + moim.getCreatedAt(), + moim.getUpdatedAt(), + userPreviewDTOList + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimIntroduceDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimIntroduceDTO.java new file mode 100644 index 00000000..c1ff13a9 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimIntroduceDTO.java @@ -0,0 +1,16 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.Moim; + +public record MoimIntroduceDTO( + String videoKeyName, + String title, + String writer, + String writerProfileImage +) { + public static MoimIntroduceDTO toMoimIntroduceDTO(Moim moim, UserProfile userProfile) { + return new MoimIntroduceDTO(moim.getIntroduceVideoKeyName(), moim.getIntroduceVideoTitle(), userProfile.getName(), userProfile.getImageUrl()); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinConfirmRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinConfirmRequestDTO.java new file mode 100644 index 00000000..6da03894 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinConfirmRequestDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.global.validation.annotation.CheckAdminValidation; + +public record MoimJoinConfirmRequestDTO( + @CheckAdminValidation + Long moimId, + Long userId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinRequestDTO.java new file mode 100644 index 00000000..dfd84e30 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinRequestDTO.java @@ -0,0 +1,23 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import com.dev.moim.domain.moim.service.impl.dto.JoinRequestDTO; + +public record MoimJoinRequestDTO( + Long userMoimId, + Long moimId, + String imageUrl, + String title, + String description, + MoimCategory moimCategory, + String location, + Integer userCounts, + JoinStatus joinStatus +) { + public static MoimJoinRequestDTO toMoimJoinRequestDTO(JoinRequestDTO joinRequestDTO, Integer userCounts) { + Moim moim = joinRequestDTO.getMoim(); + return new MoimJoinRequestDTO(joinRequestDTO.getUserMoim().getId(), moim.getId(), moim.getImageUrl(),moim.getName(), moim.getIntroduction(), moim.getMoimCategory(), moim.getLocation(), userCounts, joinRequestDTO.getUserMoim().getJoinStatus()); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinRequestListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinRequestListDTO.java new file mode 100644 index 00000000..3da8d2b6 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimJoinRequestListDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto; + +import java.util.List; + +public record MoimJoinRequestListDTO( + List moimJoinRequestDTOList, + Boolean hasNext, + Long nextCursor +) { + public static MoimJoinRequestListDTO toMoimJoinRequestListDTO(List moimJoinRequestDTOList, Boolean hasNext, Long nextCursor) { + return new MoimJoinRequestListDTO(moimJoinRequestDTOList, hasNext, nextCursor); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimPreviewDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimPreviewDTO.java new file mode 100644 index 00000000..a119f9b8 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimPreviewDTO.java @@ -0,0 +1,32 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; + +import java.time.LocalDateTime; + +public record MoimPreviewDTO( + Long moimId, + String title, + String description, + MoimCategory category, + String address, + String profileImageUrl, + Integer memberCount, + LocalDateTime createAt, + LocalDateTime updateAt +) { + public static MoimPreviewDTO toMoimPreviewDTO(Moim moim, String imageUrl) { + return new MoimPreviewDTO( + moim.getId(), + moim.getName(), + moim.getIntroduction(), + moim.getMoimCategory(), + moim.getLocation(), + imageUrl, + moim.getUserMoimList().size(), + moim.getCreatedAt(), + moim.getUpdatedAt() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimPreviewListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimPreviewListDTO.java new file mode 100644 index 00000000..00338a27 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimPreviewListDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto; + +import java.util.List; + +public record MoimPreviewListDTO( + List moimPreviewList, + Long nextCursor, + Boolean hasNext +) { + public static MoimPreviewListDTO toMoimPreviewListDTO(List moimPreviewList, Long nextCursor, Boolean hasNext) { + return new MoimPreviewListDTO(moimPreviewList, nextCursor, hasNext); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/MoimRoleResponse.java b/src/main/java/com/dev/moim/domain/moim/dto/MoimRoleResponse.java new file mode 100644 index 00000000..ac33459e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/MoimRoleResponse.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; + +public record MoimRoleResponse( + MoimRole moimRole, + JoinStatus joinStatus +) { + public static MoimRoleResponse toMoimRoleResponse(MoimRole moimRole, JoinStatus joinStatus) { + return new MoimRoleResponse(moimRole, joinStatus); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/UpdateMoimDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/UpdateMoimDTO.java new file mode 100644 index 00000000..49f32c0e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/UpdateMoimDTO.java @@ -0,0 +1,19 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import com.dev.moim.global.validation.annotation.CheckAdminValidation; +import org.hibernate.validator.constraints.Length; + +public record UpdateMoimDTO( + @CheckAdminValidation + Long moimId, + MoimCategory moimCategory, + @Length(min = 1, max = 255) + String title, + @Length(min = 1, max = 255) + String address, + @Length(min = 1, max = 255) + String description, + String imageKeyName +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/WithMoimDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/WithMoimDTO.java new file mode 100644 index 00000000..24b4be99 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/WithMoimDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.dto; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; +import org.hibernate.validator.constraints.Length; + +public record WithMoimDTO( + @UserMoimValidaton + Long moimId, + @Length(min = 1, max = 255) + String exitReason +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/MoimPlanDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/MoimPlanDTO.java new file mode 100644 index 00000000..8e077fc5 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/MoimPlanDTO.java @@ -0,0 +1,27 @@ +package com.dev.moim.domain.moim.dto.calender; + +import com.dev.moim.domain.moim.entity.Plan; + +import java.time.LocalDateTime; + +public record MoimPlanDTO( + Long planId, + String title, + String location, + String locationDetail, + LocalDateTime time, + boolean isParticipant, + int totalMemberCnt +) { + public static MoimPlanDTO from(Plan plan, boolean isParticipant) { + return new MoimPlanDTO( + plan.getId(), + plan.getTitle(), + plan.getLocation(), + plan.getLocationDetail(), + plan.getDate(), + isParticipant, + plan.getMoim().getUserMoimList().size() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanCreateDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanCreateDTO.java new file mode 100644 index 00000000..5cebf850 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanCreateDTO.java @@ -0,0 +1,19 @@ +package com.dev.moim.domain.moim.dto.calender; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +public record PlanCreateDTO( + String title, + LocalDate date, + @Schema(type = "string", example = "12:00:00") + LocalTime startTime, + String location, + String locationDetail, + String cost, + List schedules +) { +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanDayListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanDayListDTO.java new file mode 100644 index 00000000..ec3f5623 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanDayListDTO.java @@ -0,0 +1,9 @@ +package com.dev.moim.domain.moim.dto.calender; + +import java.util.List; + +public record PlanDayListDTO( + int memberWithPlanCnt, + List planList +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanDetailDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanDetailDTO.java new file mode 100644 index 00000000..865833d8 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanDetailDTO.java @@ -0,0 +1,44 @@ +package com.dev.moim.domain.moim.dto.calender; + +import com.dev.moim.domain.moim.entity.Plan; +import com.dev.moim.domain.moim.entity.Schedule; + +import java.time.LocalDateTime; +import java.util.List; + +public record PlanDetailDTO( + Long planId, + Long writerId, + String title, + LocalDateTime date, + String location, + String locationDetail, + String cost, + long participant, + List schedules, + boolean isParticipant +) { + + public static PlanDetailDTO from(Plan plan, long participant, List scheduleList, Boolean isParticipant) { + + List scheduleGetDTOList = scheduleList.stream() + .map(schedule -> new ScheduleGetDTO( + schedule.getId(), + schedule.getStartTime(), + schedule.getTitle() + )).toList(); + + return new PlanDetailDTO( + plan.getId(), + plan.getUser().getId(), + plan.getTitle(), + plan.getDate(), + plan.getLocation(), + plan.getLocationDetail(), + plan.getCost(), + participant, + scheduleGetDTOList, + isParticipant + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanMonthListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanMonthListDTO.java new file mode 100644 index 00000000..89368ae1 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanMonthListDTO.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.moim.dto.calender; + +import java.util.Map; + +public record PlanMonthListDTO( + Map planList +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanParticipantListPageDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanParticipantListPageDTO.java new file mode 100644 index 00000000..a27b5691 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/PlanParticipantListPageDTO.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.moim.dto.calender; + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.UserPlan; +import com.dev.moim.domain.user.dto.ProfileDTO; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record PlanParticipantListPageDTO( + Boolean isFirst, + Boolean hasNext, + List planParticipantList +) { + public static PlanParticipantListPageDTO from(List userProfileList, Slice userPlanSlice) { + List profileDTOList = userProfileList.stream() + .map(userProfile -> ProfileDTO.of(userProfile.getUser(), userProfile)) + .toList(); + + return new PlanParticipantListPageDTO( + userPlanSlice.isFirst(), + userPlanSlice.hasNext(), + profileDTOList + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleCreateDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleCreateDTO.java new file mode 100644 index 00000000..ce690fff --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleCreateDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.dto.calender; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalTime; + +public record ScheduleCreateDTO( + String title, + @Schema(type = "string", example = "12:00:00") + LocalTime startTime +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleGetDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleGetDTO.java new file mode 100644 index 00000000..be9e3a25 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleGetDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto.calender; + +import java.time.LocalTime; + +public record ScheduleGetDTO( + Long scheduleId, + LocalTime startTime, + String title +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleListDTO.java new file mode 100644 index 00000000..2b8bbcce --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/calender/ScheduleListDTO.java @@ -0,0 +1,22 @@ +package com.dev.moim.domain.moim.dto.calender; + +import com.dev.moim.domain.moim.entity.Schedule; + +import java.util.List; + +public record ScheduleListDTO( + List scheduleList +) { + public static ScheduleListDTO of(List scheduleList) { + List scheduleGetDTOList = scheduleList.stream() + .map(schedule -> new ScheduleGetDTO( + schedule.getId(), + schedule.getStartTime(), + schedule.getTitle() + )).toList(); + + return new ScheduleListDTO( + scheduleGetDTOList + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/AnnouncementConfirmRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/AnnouncementConfirmRequestDTO.java new file mode 100644 index 00000000..3b10dca2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/AnnouncementConfirmRequestDTO.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; + +public record AnnouncementConfirmRequestDTO( + Long postId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/AnnouncementRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/AnnouncementRequestDTO.java new file mode 100644 index 00000000..874cca81 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/AnnouncementRequestDTO.java @@ -0,0 +1,20 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.enums.PostType; +import com.dev.moim.global.validation.annotation.CheckAdminValidation; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +public record AnnouncementRequestDTO( + @CheckAdminValidation + Long moimId, + @Length(min = 1, max = 255) + String title, + @Length(min = 1, max = 1500) + String content, + List imageKeyNames, + List userIds, + Boolean isAllUserSelected +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/BlockCommentResponse.java b/src/main/java/com/dev/moim/domain/moim/dto/post/BlockCommentResponse.java new file mode 100644 index 00000000..04f03fb2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/BlockCommentResponse.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.enums.CommentStatus; + +public record BlockCommentResponse( + Long commentId, + String content, + CommentStatus commentStatus +) { + public static BlockCommentResponse toBlockCommentResponse(Comment comment) { + return new BlockCommentResponse(comment.getId(), comment.getContent(), comment.getCommentStatus()); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentBlockDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentBlockDTO.java new file mode 100644 index 00000000..7414888e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentBlockDTO.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; + +public record CommentBlockDTO( + @UserMoimValidaton + Long moimId, + Long postId, + Long commentId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentCommentResponseDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentCommentResponseDTO.java new file mode 100644 index 00000000..fafece38 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentCommentResponseDTO.java @@ -0,0 +1,45 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.CommentStatus; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public record CommentCommentResponseDTO( + Long commentId, + String content, + Integer likeCount, + String profileImage, + String writer, + Long writerId, + Boolean isLike, + CommentStatus commentStatus, + LocalDateTime updateAt, + LocalDateTime createAt +) { + public static CommentCommentResponseDTO toCommentCommentResponseDTO(Comment comment, Boolean isLike, List blockComments, Optional userMoim) { + boolean b1 = blockComments.stream().anyMatch((b) -> { + if (comment.equals(b)) { + return true; + } + return false; + } + ); + + return new CommentCommentResponseDTO( + comment.getId(), + b1 ? null : comment.getContent(), + comment.getCommentLikeList().size(), + userMoim.isEmpty() || b1 ? null : userMoim.get().getUserProfile().getImageUrl(), + userMoim.isEmpty() || b1 ? null : userMoim.get().getUserProfile().getName(), + userMoim.isEmpty() || b1 ? null : userMoim.get().getUser().getId(), + isLike, + b1 ? CommentStatus.BLOCKED : comment.getCommentStatus(), + comment.getUpdatedAt(), + comment.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentLikeDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentLikeDTO.java new file mode 100644 index 00000000..cb840e19 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentLikeDTO.java @@ -0,0 +1,6 @@ +package com.dev.moim.domain.moim.dto.post; + +public record CommentLikeDTO( + Long commentId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentReportDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentReportDTO.java new file mode 100644 index 00000000..6e5db5c7 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentReportDTO.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; + +public record CommentReportDTO( + @UserMoimValidaton + Long moimId, + Long postId, + Long commentId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentResponseDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentResponseDTO.java new file mode 100644 index 00000000..a77a5838 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentResponseDTO.java @@ -0,0 +1,50 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.CommentStatus; +import com.dev.moim.domain.moim.entity.enums.PostType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public record CommentResponseDTO( + Long commentId, + String content, + Integer likeCount, + Long writerId, + String profileImage, + String writer, + Boolean isLike, + CommentStatus commentStatus, + LocalDateTime updateAt, + LocalDateTime createAt, + List commentResponseDTOList +) { + public static CommentResponseDTO toCommentResponseDTO(Comment comment, Boolean isLike, List commentResponseDTOList, List blockComments, Optional userMoim) { + + boolean b1 = blockComments.stream().anyMatch((b) -> { + if (comment.equals(b)) { + return true; + } + return false; + } + ); + + return new CommentResponseDTO( + comment.getId(), + b1 ? null : comment.getContent(), + comment.getCommentLikeList().size(), + userMoim.isEmpty() || b1 ? null : userMoim.get().getUser().getId(), + userMoim.isEmpty() || b1 ? null : userMoim.get().getUserProfile().getImageUrl(), + userMoim.isEmpty() || b1 ? null : userMoim.get().getUserProfile().getName(), + isLike, + b1 ? CommentStatus.BLOCKED : comment.getCommentStatus(), + comment.getUpdatedAt(), + comment.getCreatedAt(), + commentResponseDTOList + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentResponseListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentResponseListDTO.java new file mode 100644 index 00000000..3821202c --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentResponseListDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto.post; + +import java.util.List; + +public record CommentResponseListDTO( + List moimPreviewList, + Long nextCursor, + Boolean hasNext +) { + public static CommentResponseListDTO toCommentResponseListDTO(List commentLists, Long nextCursor, Boolean hasNext) { + return new CommentResponseListDTO(commentLists, nextCursor, hasNext); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CommentUpdateRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentUpdateRequestDTO.java new file mode 100644 index 00000000..6a7e1166 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CommentUpdateRequestDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto.post; + +import org.hibernate.validator.constraints.Length; + +public record CommentUpdateRequestDTO( + Long commentId, + @Length(min = 1, max = 255) + String content +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentCommentDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentCommentDTO.java new file mode 100644 index 00000000..8688d105 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentCommentDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.dto.post; + +import org.hibernate.validator.constraints.Length; + +public record CreateCommentCommentDTO( + Long moimId, + Long commentId, + Long postId, + @Length(min = 1, max = 255) + String content +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentDTO.java new file mode 100644 index 00000000..9674e51e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentDTO.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.enums.PostType; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +public record CreateCommentDTO( + Long moimId, + Long postId, + @Length(min = 1, max = 255) + String content +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentResultDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentResultDTO.java new file mode 100644 index 00000000..ccd39097 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateCommentResultDTO.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Comment; + +import java.time.LocalDateTime; + +public record CreateCommentResultDTO( + Long moimPostId, + LocalDateTime createAt, + LocalDateTime updateAt +) { + public static CreateCommentResultDTO toCreateCommentResultDTO(Comment comment) { + return new CreateCommentResultDTO(comment.getId(), comment.getCreatedAt(), comment.getUpdatedAt()); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CreateMoimPostDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateMoimPostDTO.java new file mode 100644 index 00000000..797d274c --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateMoimPostDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.enums.PostType; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +public record CreateMoimPostDTO( + Long moimId, + @Length(min = 1, max = 255) + String title, + @Length(min = 1, max = 1500) + String content, + List imageKeyNames, + PostType postType +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/CreateMoimPostResultDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateMoimPostResultDTO.java new file mode 100644 index 00000000..e1217a39 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/CreateMoimPostResultDTO.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Post; + +import java.time.LocalDateTime; + +public record CreateMoimPostResultDTO( + Long moimPostId, + LocalDateTime createAt, + LocalDateTime updateAt +) { + public static CreateMoimPostResultDTO toCreateMoimPostDTO(Post post) { + return new CreateMoimPostResultDTO(post.getId(), post.getCreatedAt(), post.getUpdatedAt()); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/JoinMoimPostsResponseDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/JoinMoimPostsResponseDTO.java new file mode 100644 index 00000000..1c418fb6 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/JoinMoimPostsResponseDTO.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.moim.dto.post; + +import java.util.List; + +public record JoinMoimPostsResponseDTO( + Long moimId, + String moimTitle, + List moimPostPreviewDTOList +) { + public static JoinMoimPostsResponseDTO toJoinMoimPostsResponseDTO(Long moimdId, String moimTitle, List moimPostPreviewDTOList) { + return new JoinMoimPostsResponseDTO(moimdId, moimTitle, moimPostPreviewDTOList); + } + +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/LikeResultDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/LikeResultDTO.java new file mode 100644 index 00000000..ebdcfbd4 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/LikeResultDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto.post; + +public record LikeResultDTO + ( + Boolean isLike +) { + public static LikeResultDTO toLikeResultDTO(Boolean isLike) { + return new LikeResultDTO(isLike); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostDetailDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostDetailDTO.java new file mode 100644 index 00000000..d8894f05 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostDetailDTO.java @@ -0,0 +1,48 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.PostImage; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.PostType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public record MoimPostDetailDTO( + Long moimPostId, + String title, + String content, + Long writerId, + String profileImage, + String writer, + Integer commentCount, + Integer likeCount, + Boolean isLike, + PostType postType, + List imageKeyNames, + LocalDateTime updateAt, + LocalDateTime createAt +) { + public static MoimPostDetailDTO toMoimPostDetailDTO(Post post, Boolean postLike, Optional userMoim) { + + List imageKeyNames = post.getPostImageList().stream().map(PostImage::getImageKeyName).toList(); + + return new MoimPostDetailDTO( + post.getId(), + post.getTitle(), + post.getContent(), + userMoim.isEmpty() ? null : userMoim.get().getUserProfile().getId(), + userMoim.isEmpty() ? null : userMoim.get().getUserProfile().getImageUrl(), + userMoim.isEmpty() ? null : userMoim.get().getUserProfile().getName(), + post.getCommentList().size(), + post.getPostLikeList().size(), + postLike, + post.getPostType(), + imageKeyNames, + post.getUpdatedAt(), + post.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostPreviewDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostPreviewDTO.java new file mode 100644 index 00000000..7e9911d2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostPreviewDTO.java @@ -0,0 +1,42 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.PostImage; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.PostType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public record MoimPostPreviewDTO( + Long moimPostId, + Long moimId, + String title, + String content, + String moimImageUrl, + String ownerProfileImageUrl, + String writer, + Long writerId, + Integer commentCount, + Integer likeCount, + PostType postType, + LocalDateTime createAt +) { + public static MoimPostPreviewDTO toMoimPostPreviewDTO(Post post, Optional userMoim) { + return new MoimPostPreviewDTO( + post.getId(), + post.getMoim().getId(), + post.getTitle(), + post.getContent(), + post.getMoim().getImageUrl(), + userMoim.isEmpty() ? null : userMoim.get().getUserProfile().getImageUrl(), + userMoim.isEmpty() ? null : userMoim.get().getUserProfile().getName(), + userMoim.isEmpty() ? null : userMoim.get().getUser().getId(), + post.getCommentList().size(), + post.getPostLikeList().size(), + post.getPostType(), + post.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostPreviewListDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostPreviewListDTO.java new file mode 100644 index 00000000..a0c0bd7a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/MoimPostPreviewListDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.dto.post; + +import java.util.List; + +public record MoimPostPreviewListDTO( + List moimPreviewList, + Long nextCursor, + Boolean hasNext +) { + public static MoimPostPreviewListDTO toMoimPostPreviewListDTO(List moimPreviewList, Long nextCursor, Boolean hasNext) { + return new MoimPostPreviewListDTO(moimPreviewList, nextCursor, hasNext); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/PostBlockDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/PostBlockDTO.java new file mode 100644 index 00000000..76fff7e6 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/PostBlockDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; + +public record PostBlockDTO( + @UserMoimValidaton + Long moimId, + Long postId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/PostLikeDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/PostLikeDTO.java new file mode 100644 index 00000000..90d91f1e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/PostLikeDTO.java @@ -0,0 +1,6 @@ +package com.dev.moim.domain.moim.dto.post; + +public record PostLikeDTO( + Long postId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/PostReportDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/PostReportDTO.java new file mode 100644 index 00000000..87301417 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/PostReportDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; + +public record PostReportDTO( + @UserMoimValidaton + Long moimId, + Long postId +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/UpdateMoimPostDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/UpdateMoimPostDTO.java new file mode 100644 index 00000000..fb02db8d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/UpdateMoimPostDTO.java @@ -0,0 +1,18 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.global.validation.annotation.UserMoimValidaton; +import org.hibernate.validator.constraints.Length; + +import java.util.List; + +public record UpdateMoimPostDTO( + @UserMoimValidaton + Long moimId, + Long postId, + @Length(min = 1, max = 255) + String title, + @Length(min = 1, max = 1500) + String content, + List imageKeyNames +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/post/UpdatePostResponseDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/post/UpdatePostResponseDTO.java new file mode 100644 index 00000000..6c3dcfab --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/post/UpdatePostResponseDTO.java @@ -0,0 +1,21 @@ +package com.dev.moim.domain.moim.dto.post; + +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.PostImage; +import com.dev.moim.domain.moim.entity.enums.PostType; + +import java.util.List; + +public record UpdatePostResponseDTO( + Long postId, + String title, + String content, + PostType postType, + List images +) { + public static UpdatePostResponseDTO toUpdatePostResponseDTO(Post post) { + List images = post.getPostImageList().stream().map((i) -> i.getImageKeyName()).toList(); + + return new UpdatePostResponseDTO(post.getId(), post.getTitle(), post.getContent(), post.getPostType(), images); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/AddTodoAssigneeDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/AddTodoAssigneeDTO.java new file mode 100644 index 00000000..b51aaf9d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/AddTodoAssigneeDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.global.validation.annotation.AddAssigneeValidation; +import com.dev.moim.global.validation.annotation.CheckAdminValidation; +import com.dev.moim.global.validation.annotation.MoimValidation; +import com.dev.moim.global.validation.annotation.TodoValidation; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +@AddAssigneeValidation +public record AddTodoAssigneeDTO( + @CheckAdminValidation @MoimValidation Long moimId, + @TodoValidation Long todoId, + @NotNull List addAssigneeIdList +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/CreateTodoDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/CreateTodoDTO.java new file mode 100644 index 00000000..74cebb6a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/CreateTodoDTO.java @@ -0,0 +1,20 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.global.validation.annotation.TodoTargetUserValidation; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.List; + +@TodoTargetUserValidation +public record CreateTodoDTO( + Long moimId, + String title, + String content, + LocalDate dueDate, + List imageKeyList, + List targetUserIdList, + @Schema(description = "멤버 전체 선택 여부", defaultValue = "false") + boolean isAssigneeSelectAll +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/DeleteTodoAssigneeDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/DeleteTodoAssigneeDTO.java new file mode 100644 index 00000000..685fd4b7 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/DeleteTodoAssigneeDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.global.validation.annotation.CheckAdminValidation; +import com.dev.moim.global.validation.annotation.DeleteAssigneeValidation; +import com.dev.moim.global.validation.annotation.MoimValidation; +import com.dev.moim.global.validation.annotation.TodoValidation; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +@DeleteAssigneeValidation +public record DeleteTodoAssigneeDTO( + @CheckAdminValidation @MoimValidation Long moimId, + @TodoValidation Long todoId, + @NotNull List deleteAssigneeIdList +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoAssigneeDetailDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoAssigneeDetailDTO.java new file mode 100644 index 00000000..68a9c332 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoAssigneeDetailDTO.java @@ -0,0 +1,30 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.UserTodo; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; + +public record TodoAssigneeDetailDTO( + Long assigneeId, + String nickname, + String profileImageUrl, + TodoAssigneeStatus todoAssigneeStatus +) { + public static TodoAssigneeDetailDTO toTodoAssignee(UserTodo userTodo, UserMoim userMoim) { + return new TodoAssigneeDetailDTO( + userTodo.getUser().getId(), + userMoim.getUserProfile().getName(), + userMoim.getUserProfile().getImageUrl(), + userTodo.getStatus() + ); + } + + public static TodoAssigneeDetailDTO toTodoNonAssignee(UserMoim userMoim) { + return new TodoAssigneeDetailDTO( + userMoim.getUser().getId(), + userMoim.getUserProfile().getName(), + userMoim.getUserProfile().getImageUrl(), + null + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoDTO.java new file mode 100644 index 00000000..2321cabe --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoDTO.java @@ -0,0 +1,75 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.TodoImage; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.UserTodo; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.domain.moim.entity.enums.TodoStatus; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public record TodoDTO( + Long todoId, + String title, + LocalDateTime dueDate, + List imageUrlList, + TodoStatus todoStatus, + TodoAssigneeStatus todoAssigneeStatus, + String writerNickname, + String writerProfileImageUrl, + MoimRole writerMoimRole, + Long moimId, + String moimName +) { + public static TodoDTO forMoimAdmins(Todo todo, Optional userMoim) { + return new TodoDTO( + todo.getId(), + todo.getTitle(), + todo.getDueDate(), + todo.getTodoImageList().stream().map(TodoImage::getImageUrl).toList(), + todo.getStatus(), + null, + userMoim.map(moim -> moim.getUserProfile().getName()).orElse(null), + userMoim.map(value -> value.getUserProfile().getImageUrl()).orElse(null), + userMoim.map(UserMoim::getMoimRole).orElse(null), + todo.getMoim().getId(), + todo.getMoim().getName() + ); + } + + public static TodoDTO forSpecificAdmin(Todo todo) { + return new TodoDTO( + todo.getId(), + todo.getTitle(), + todo.getDueDate(), + todo.getTodoImageList().stream().map(TodoImage::getImageUrl).toList(), + todo.getStatus(), + null, + null, + null, + null, + todo.getMoim().getId(), + todo.getMoim().getName() + ); + } + + public static TodoDTO forAssignee(Todo todo, UserTodo userTodo) { + return new TodoDTO( + todo.getId(), + todo.getTitle(), + todo.getDueDate(), + todo.getTodoImageList().stream().map(TodoImage::getImageUrl).toList(), + todo.getStatus(), + userTodo.getStatus(), + null, + null, + null, + todo.getMoim().getId(), + todo.getMoim().getName() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoDetailDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoDetailDTO.java new file mode 100644 index 00000000..8ef03eb3 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoDetailDTO.java @@ -0,0 +1,19 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.domain.moim.entity.enums.TodoStatus; + +import java.time.LocalDateTime; +import java.util.List; + +public record TodoDetailDTO( + Long todoId, + Long moimId, + String title, + String content, + LocalDateTime dueDate, + List imageUrlList, + TodoAssigneeStatus todoAssigneeStatus, + TodoStatus todoStatus +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoPageDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoPageDTO.java new file mode 100644 index 00000000..3f219c18 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/TodoPageDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.dto.todo; + +import java.util.List; + +public record TodoPageDTO( + List list, + Long nextCursor, + Boolean hasNext +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoDTO.java new file mode 100644 index 00000000..3ba80b4c --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoDTO.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.global.validation.annotation.TodoUpdateDueDateValidation; + +import java.time.LocalDate; +import java.util.List; + +public record UpdateTodoDTO( + String title, + String content, + @TodoUpdateDueDateValidation LocalDate dueDate, + List imageKeyList +) { +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoStatusDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoStatusDTO.java new file mode 100644 index 00000000..b858b485 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoStatusDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.global.validation.annotation.TodoAssigneeStatusValidation; +import com.dev.moim.global.validation.annotation.TodoAssigneeValidation; + +@TodoAssigneeStatusValidation +public record UpdateTodoStatusDTO( + @TodoAssigneeValidation Long todoId, + TodoAssigneeStatus todoAssigneeStatus +) { +} diff --git a/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoStatusResponseDTO.java b/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoStatusResponseDTO.java new file mode 100644 index 00000000..e9911621 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/dto/todo/UpdateTodoStatusResponseDTO.java @@ -0,0 +1,19 @@ +package com.dev.moim.domain.moim.dto.todo; + +import com.dev.moim.domain.moim.entity.UserTodo; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.domain.moim.entity.enums.TodoStatus; + +public record UpdateTodoStatusResponseDTO( + Long todoId, + TodoAssigneeStatus todoAssigneeStatus, + TodoStatus todoStatus +) { + public static UpdateTodoStatusResponseDTO of(UserTodo userTodo) { + return new UpdateTodoStatusResponseDTO( + userTodo.getId(), + userTodo.getStatus(), + userTodo.getTodo().getStatus() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Calendar.java b/src/main/java/com/dev/moim/domain/moim/entity/Calendar.java deleted file mode 100644 index 1568886e..00000000 --- a/src/main/java/com/dev/moim/domain/moim/entity/Calendar.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.dev.moim.domain.moim.entity; - -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Calendar extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "is_attention") - private Boolean isAttention; - - private String title; - - // 일정 날짜 - private LocalDate date; - - // 활동 지역 - private String location; - - // 비용 - private String cost; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_group_id") - private UserMoim userMoim; - - @OneToMany(mappedBy = "calendar") - private List scheduleList = new ArrayList<>(); -} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Category.java b/src/main/java/com/dev/moim/domain/moim/entity/Category.java deleted file mode 100644 index 0e7bb675..00000000 --- a/src/main/java/com/dev/moim/domain/moim/entity/Category.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.dev.moim.domain.moim.entity; - -import com.dev.moim.domain.moim.entity.enums.CategoryType; -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Category extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - @Enumerated(EnumType.STRING) - private CategoryType categoryType; - - @OneToMany(mappedBy = "category") - private List moimCategoryList = new ArrayList<>(); - - @OneToMany(mappedBy = "category") - private List postCategoryList = new ArrayList<>(); -} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Comment.java b/src/main/java/com/dev/moim/domain/moim/entity/Comment.java index a919a9b5..e2d4af24 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/Comment.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/Comment.java @@ -1,19 +1,17 @@ package com.dev.moim.domain.moim.entity; +import com.dev.moim.domain.moim.entity.enums.CommentStatus; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.checkerframework.checker.units.qual.C; + +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -28,17 +26,41 @@ public class Comment extends BaseEntity { private String content; - @Column(name = "is_anonymous") - private Boolean isAnonymous; - - // 신고 횟수 - private Integer report; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_group_id") + @JoinColumn(name = "user_moim_id") private UserMoim userMoim; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ACTIVE'") + private CommentStatus commentStatus; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List commentLikeList = new ArrayList<>(); + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List commentReportList = new ArrayList<>(); + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List commentBlockList = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Comment parent; + + @OneToMany(mappedBy = "parent", orphanRemoval = true) + private List children = new ArrayList<>(); + + public void delete() { + this.content = null; + this.userMoim = null; + this.commentStatus = CommentStatus.DELETED; + } + + public void update(String content) { + this.content = content; + } } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/CommentBlock.java b/src/main/java/com/dev/moim/domain/moim/entity/CommentBlock.java new file mode 100644 index 00000000..1e81f3e6 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/CommentBlock.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CommentBlock extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_block_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/GroupAlarm.java b/src/main/java/com/dev/moim/domain/moim/entity/CommentLike.java similarity index 71% rename from src/main/java/com/dev/moim/domain/moim/entity/GroupAlarm.java rename to src/main/java/com/dev/moim/domain/moim/entity/CommentLike.java index 0d1b93b5..e1716eb6 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/GroupAlarm.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/CommentLike.java @@ -1,5 +1,6 @@ package com.dev.moim.domain.moim.entity; +import com.dev.moim.domain.account.entity.User; import com.dev.moim.global.common.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -9,7 +10,6 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -21,17 +21,17 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -@Table(name = "group_alarm") -public class GroupAlarm extends BaseEntity { - +public class CommentLike extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "group_alarm_id") + @Column(name = "comment_like_id") private Long id; - private String content; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_group_id") - private UserMoim userMoim; + @JoinColumn(name = "comment_id") + private Comment comment; } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/CommentReport.java b/src/main/java/com/dev/moim/domain/moim/entity/CommentReport.java new file mode 100644 index 00000000..93a6dbc3 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/CommentReport.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CommentReport extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_report_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Dashboard.java b/src/main/java/com/dev/moim/domain/moim/entity/Dashboard.java deleted file mode 100644 index cc5fb8e9..00000000 --- a/src/main/java/com/dev/moim/domain/moim/entity/Dashboard.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.dev.moim.domain.moim.entity; - -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Table(name = "dashboard") -public class Dashboard extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(fetch = FetchType.LAZY) - private Moim moim; -} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/ExitReason.java b/src/main/java/com/dev/moim/domain/moim/entity/ExitReason.java index ab4e3ee7..387f24b9 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/ExitReason.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/ExitReason.java @@ -1,13 +1,8 @@ package com.dev.moim.domain.moim.entity; +import com.dev.moim.domain.account.entity.User; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -25,9 +20,13 @@ public class ExitReason extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String opinion; + private String content; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_group_id") - private UserMoim userMoim; + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "moim_id") + private Moim moim; } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/IndividualPlan.java b/src/main/java/com/dev/moim/domain/moim/entity/IndividualPlan.java new file mode 100644 index 00000000..70ed3ccf --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/IndividualPlan.java @@ -0,0 +1,52 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@DynamicInsert +@DynamicUpdate +public class IndividualPlan extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @Column(nullable = false) + private LocalDateTime date; + + private String location; + + private String locationDetail; + + private String memo; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public void updateIndividualPlan( + String title, + LocalDateTime date, + String location, + String locationDetail, + String memo) { + this.title = title; + this.date = date; + this.location = location; + this.locationDetail = locationDetail; + this.memo = memo; + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Moim.java b/src/main/java/com/dev/moim/domain/moim/entity/Moim.java index ddea93cd..07139dcf 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/Moim.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/Moim.java @@ -1,18 +1,8 @@ package com.dev.moim.domain.moim.entity; -import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -40,28 +30,25 @@ public class Moim extends BaseEntity { private String introduction; @Enumerated(EnumType.STRING) - private UserMoim.MoimType moimType; + private MoimCategory moimCategory; - @Column(name = "moim_image") - private String moimImage; + private String introduceVideoKeyName; - @OneToMany(mappedBy = "moim") - private List userMoimList = new ArrayList<>(); - - @OneToMany(mappedBy = "moim") - private List moimAccountList = new ArrayList<>(); + private String introduceVideoTitle; - @OneToMany(mappedBy = "moim") - private List moimTagList = new ArrayList<>(); + private String imageUrl; - @OneToMany(mappedBy = "moim") - private List moimCategoryList = new ArrayList<>(); + @OneToMany(mappedBy = "moim", cascade = CascadeType.ALL, orphanRemoval = true) + private List userMoimList = new ArrayList<>(); - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "dashboard_id") - private Dashboard dashboard; + @OneToMany(mappedBy = "moim", cascade = CascadeType.ALL, orphanRemoval = true) + private List exitReasonList = new ArrayList<>(); - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_profile_id") - private UserProfile userProfile; + public void updateMoim(String title, MoimCategory moimCategory, String location, String introduction, String moimUrl) { + this.name = title; + this.moimCategory = moimCategory; + this.location = location; + this.introduction = introduction; + this.imageUrl = moimUrl; + } } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/MoimAccount.java b/src/main/java/com/dev/moim/domain/moim/entity/MoimAccount.java deleted file mode 100644 index 2e3f76c7..00000000 --- a/src/main/java/com/dev/moim/domain/moim/entity/MoimAccount.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.dev.moim.domain.moim.entity; - -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class MoimAccount extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String account; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "moim_id") - private Moim moim; -} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Plan.java b/src/main/java/com/dev/moim/domain/moim/entity/Plan.java new file mode 100644 index 00000000..6ce5a984 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/Plan.java @@ -0,0 +1,79 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@DynamicInsert +@DynamicUpdate +public class Plan extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + private LocalDateTime date; + + private String location; + + private String locationDetail; + + private String cost; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "moim_id") + private Moim moim; + + @OneToMany(mappedBy = "plan", cascade = CascadeType.ALL, orphanRemoval = true) + private List scheduleList = new ArrayList<>(); + + @OneToMany(mappedBy = "plan", cascade = CascadeType.REMOVE) + private List userPlanList = new ArrayList<>(); + + public void addSchedule(Schedule schedule) { + scheduleList.add(schedule); + schedule.assignPlan(this); + } + + public void updatePlan( + String title, + LocalDateTime date, + String location, + String locationDetail, + String cost) { + this.title = title; + this.date = date; + this.location = location; + this.locationDetail = locationDetail; + this.cost = cost; + } + + public void updateSchedule(List scheduleList) { + this.scheduleList.clear(); + for(Schedule schedule : scheduleList) { + this.addSchedule(schedule); + } + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Post.java b/src/main/java/com/dev/moim/domain/moim/entity/Post.java index 6f842ec5..7395ba7a 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/Post.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/Post.java @@ -1,19 +1,15 @@ package com.dev.moim.domain.moim.entity; +import com.dev.moim.domain.moim.entity.enums.PostType; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.util.ArrayList; import java.util.List; @@ -29,14 +25,42 @@ public class Post extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToMany(mappedBy = "post") - private List commentList = new ArrayList<>(); + private String title; + + @Column(length = 1500) + private String content; - @OneToMany(mappedBy = "post") - private List postCategoryList = new ArrayList<>(); + @Enumerated(EnumType.STRING) + private PostType postType; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "moim_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private Moim moim; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_group_id") + @JoinColumn(name = "user_moim_id") private UserMoim userMoim; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List commentList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postLikeList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postImageList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postBlockList = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List readPostList = new ArrayList<>(); + + public void updatePost(String title, String content, List postImages) { + this.title = title; + this.content = content; + postImageList.clear(); + postImageList.addAll(postImages); + } } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/PostBlock.java b/src/main/java/com/dev/moim/domain/moim/entity/PostBlock.java new file mode 100644 index 00000000..a23b9352 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/PostBlock.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PostBlock extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_block_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/PostCategory.java b/src/main/java/com/dev/moim/domain/moim/entity/PostImage.java similarity index 79% rename from src/main/java/com/dev/moim/domain/moim/entity/PostCategory.java rename to src/main/java/com/dev/moim/domain/moim/entity/PostImage.java index 205131c4..5f4fa3c0 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/PostCategory.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/PostImage.java @@ -1,6 +1,8 @@ package com.dev.moim.domain.moim.entity; +import com.dev.moim.domain.account.entity.User; import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -8,7 +10,6 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -20,17 +21,15 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class PostCategory extends BaseEntity { - +public class PostImage extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_image_id") private Long id; + private String imageKeyName; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "category_id") - private Category category; } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/MoimCategory.java b/src/main/java/com/dev/moim/domain/moim/entity/PostLike.java similarity index 73% rename from src/main/java/com/dev/moim/domain/moim/entity/MoimCategory.java rename to src/main/java/com/dev/moim/domain/moim/entity/PostLike.java index 9fc71294..6448c9d8 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/MoimCategory.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/PostLike.java @@ -1,6 +1,8 @@ package com.dev.moim.domain.moim.entity; +import com.dev.moim.domain.account.entity.User; import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -19,17 +21,18 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class MoimCategory extends BaseEntity { +public class PostLike extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_like_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "moim_id") - private Moim moim; + @JoinColumn(name = "user_id") + private User user; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "category_id") - private Category category; + @JoinColumn(name = "post_id") + private Post post; } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/PostReport.java b/src/main/java/com/dev/moim/domain/moim/entity/PostReport.java new file mode 100644 index 00000000..0d433b40 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/PostReport.java @@ -0,0 +1,27 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PostReport extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_report_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/ReadPost.java b/src/main/java/com/dev/moim/domain/moim/entity/ReadPost.java new file mode 100644 index 00000000..6775475b --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/ReadPost.java @@ -0,0 +1,34 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ReadPost extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "read_post_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE") + private Boolean isRead; + + public void read() { + this.isRead = true; + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Schedule.java b/src/main/java/com/dev/moim/domain/moim/entity/Schedule.java index 1fadf8c8..271a0ecc 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/Schedule.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/Schedule.java @@ -9,39 +9,39 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; -import java.time.LocalDateTime; +import java.time.LocalTime; @Entity @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@DynamicInsert +@DynamicUpdate public class Schedule extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String content; + private String title; @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "end_time") - private LocalDateTime endTime; - - @Column(name = "alert_time") - private LocalDateTime alertTime; - + private LocalTime startTime; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "calendar_id") - private Calendar calendar; + @JoinColumn(name = "plan_id") + private Plan plan; + + public void assignPlan(Plan plan) { + this.plan = plan; + } } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/TakingOverPost.java b/src/main/java/com/dev/moim/domain/moim/entity/TakingOverPost.java deleted file mode 100644 index 83f53506..00000000 --- a/src/main/java/com/dev/moim/domain/moim/entity/TakingOverPost.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.dev.moim.domain.moim.entity; - -import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class TakingOverPost extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String title; - - private String content; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_group_id") - private UserMoim userMoim; -} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/Todo.java b/src/main/java/com/dev/moim/domain/moim/entity/Todo.java new file mode 100644 index 00000000..63b68736 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/Todo.java @@ -0,0 +1,77 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.enums.TodoStatus; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Todo extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + private String content; + + @Column(nullable = false) + private LocalDateTime dueDate; + + @Enumerated(EnumType.STRING) + @ColumnDefault("'IN_PROGRESS'") + @Column(nullable = false) + private TodoStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id") + private User writer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "moim_id") + private Moim moim; + + @OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true) + private List userTodoList = new ArrayList<>(); + + @OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true) + private List todoImageList = new ArrayList<>(); + + public void updateWriter(User writer) { + this.writer = writer; + } + + public void updateTodo( + String title, + String content, + LocalDateTime dueDate, + List newImageList + ) { + this.title = title; + this.content = content; + this.dueDate = dueDate; + + if (newImageList == null) { + this.todoImageList.clear(); + } else { + this.todoImageList.clear(); + this.todoImageList.addAll(newImageList); + } + } + + public void updateStatus(TodoStatus status) { + this.status = status; + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/TodoImage.java b/src/main/java/com/dev/moim/domain/moim/entity/TodoImage.java new file mode 100644 index 00000000..1cccad31 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/TodoImage.java @@ -0,0 +1,23 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TodoImage extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String imageUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "todo_id") + private Todo todo; +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/UserMoim.java b/src/main/java/com/dev/moim/domain/moim/entity/UserMoim.java index da0f2da4..6cb93f7a 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/UserMoim.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/UserMoim.java @@ -1,32 +1,27 @@ package com.dev.moim.domain.moim.entity; import com.dev.moim.domain.account.entity.User; -import com.dev.moim.domain.moim.entity.enums.Role; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.entity.enums.ProfileStatus; import com.dev.moim.global.common.BaseEntity; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@SQLDelete(sql = "UPDATE user_moim SET join_status = 'DELETED' WHERE id = ?") +@SQLRestriction(value = "join_status <> 'DELETED'") public class UserMoim extends BaseEntity { @Id @@ -34,7 +29,19 @@ public class UserMoim extends BaseEntity { private Long id; @Enumerated(EnumType.STRING) - private Role role; + @Column(name = "moim_role", nullable = false) + private MoimRole moimRole; + + @Enumerated(EnumType.STRING) + @Column(name = "join_status", nullable = false) + private JoinStatus joinStatus; + + @Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE") + private Boolean confirm; + + @Enumerated(EnumType.STRING) + @Column(name = "profile_status", nullable = false) + private ProfileStatus profileStatus; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @@ -44,25 +51,39 @@ public class UserMoim extends BaseEntity { @JoinColumn(name = "moim_id") private Moim moim; - @OneToMany(mappedBy = "userMoim") - private List calendarList = new ArrayList<>(); + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_profile_id") + private UserProfile userProfile; + + public void accept() { + this.joinStatus = JoinStatus.COMPLETE; + } + + public void reject() { + this.joinStatus = JoinStatus.REJECT; + } - @OneToMany(mappedBy = "userMoim") - private List groupAlarmList = new ArrayList<>(); + public void changeStatus(MoimRole moimRole) { + this.moimRole = moimRole; + } - @OneToMany(mappedBy = "userMoim") - private List exitReasonList = new ArrayList<>(); + public void updateProfileStatus (ProfileStatus profileStatus) { + this.profileStatus = profileStatus; + } - @OneToMany(mappedBy = "userMoim") - private List takingOverPostList = new ArrayList<>(); + public void leaveOwner () { + this.moimRole = MoimRole.ADMIN; + } - @OneToMany(mappedBy = "userMoim") - private List postList = new ArrayList<>(); + public void enterOwner () { + this.moimRole = MoimRole.OWNER; + } - @OneToMany(mappedBy = "userMoim") - private List commentList = new ArrayList<>(); + public void confirm () { + this.confirm = true; + } - public enum MoimType { - COMMON, THUNDER + public void updateUserProfile(UserProfile userProfile) { + this.userProfile = userProfile; } } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/UserPlan.java b/src/main/java/com/dev/moim/domain/moim/entity/UserPlan.java new file mode 100644 index 00000000..783a2fa0 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/UserPlan.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserPlan extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "plan_id") + private Plan plan; +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/UserTodo.java b/src/main/java/com/dev/moim/domain/moim/entity/UserTodo.java new file mode 100644 index 00000000..fe6575b2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/UserTodo.java @@ -0,0 +1,37 @@ +package com.dev.moim.domain.moim.entity; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserTodo extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @ColumnDefault("'PENDING'") + @Column(nullable = false) + private TodoAssigneeStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "todo_id") + private Todo todo; + + public void updateStatus(TodoAssigneeStatus todoAssigneeStatus) { + this.status = todoAssigneeStatus; + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/CommentStatus.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/CommentStatus.java new file mode 100644 index 00000000..53ce582d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/CommentStatus.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum CommentStatus { + ACTIVE, DELETED, BLOCKED +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/JoinStatus.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/JoinStatus.java new file mode 100644 index 00000000..5ac462ae --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/JoinStatus.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum JoinStatus { + COMPLETE, LOADING, REJECT, DELETED +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/MoimCategory.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/MoimCategory.java new file mode 100644 index 00000000..d008187e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/MoimCategory.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum MoimCategory { + SPORTS, TECH, HUMANITY, LANGUAGE, ARTICLE, VOLUNTEER, RELIGION, PHOTO, ANIMAL, MUSIC, SELF, ETC +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/CategoryType.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/MoimRole.java similarity index 51% rename from src/main/java/com/dev/moim/domain/moim/entity/enums/CategoryType.java rename to src/main/java/com/dev/moim/domain/moim/entity/enums/MoimRole.java index 5d9e2d9b..a590e03e 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/enums/CategoryType.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/MoimRole.java @@ -1,5 +1,5 @@ package com.dev.moim.domain.moim.entity.enums; -public enum CategoryType { - GROUP, POST +public enum MoimRole { + OWNER, ADMIN, MEMBER } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/PlanType.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/PlanType.java new file mode 100644 index 00000000..46d6f0bc --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/PlanType.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum PlanType { + MOIM_PLAN, INDIVIDUAL_PLAN, TODO_PLAN; + + public static PlanType fromString(String type) { + try { + return PlanType.valueOf(type); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/PostType.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/PostType.java new file mode 100644 index 00000000..0789de16 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/PostType.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum PostType { + ANNOUNCEMENT, REVIEW, WELCOME, FREE, GLOBAL +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/Role.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/ProfileStatus.java similarity index 51% rename from src/main/java/com/dev/moim/domain/moim/entity/enums/Role.java rename to src/main/java/com/dev/moim/domain/moim/entity/enums/ProfileStatus.java index 90e73615..62a7443c 100644 --- a/src/main/java/com/dev/moim/domain/moim/entity/enums/Role.java +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/ProfileStatus.java @@ -1,5 +1,5 @@ package com.dev.moim.domain.moim.entity.enums; -public enum Role { - ADMIN, COMMON +public enum ProfileStatus { + PUBLIC, PRIVATE } diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/TodoAssigneeStatus.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/TodoAssigneeStatus.java new file mode 100644 index 00000000..70a877b5 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/TodoAssigneeStatus.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum TodoAssigneeStatus { + PENDING, LOADING, COMPLETE, OVERDUE +} diff --git a/src/main/java/com/dev/moim/domain/moim/entity/enums/TodoStatus.java b/src/main/java/com/dev/moim/domain/moim/entity/enums/TodoStatus.java new file mode 100644 index 00000000..a564622b --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/entity/enums/TodoStatus.java @@ -0,0 +1,5 @@ +package com.dev.moim.domain.moim.entity.enums; + +public enum TodoStatus { + IN_PROGRESS, COMPLETED, EXPIRED +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/CommentBlockRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/CommentBlockRepository.java new file mode 100644 index 00000000..a6517b51 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/CommentBlockRepository.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.CommentBlock; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface CommentBlockRepository extends JpaRepository { + Optional findByUserAndComment(User user, Comment comment); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/CommentLikeRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/CommentLikeRepository.java new file mode 100644 index 00000000..c34b62cc --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/CommentLikeRepository.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.CommentLike; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CommentLikeRepository extends JpaRepository { + Boolean existsCommentLikeByUserIdAndCommentId(Long userId, Long commentId); + Optional findCommentLikeByUserIdAndCommentId(Long userId, Long commentId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/CommentReportRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/CommentReportRepository.java new file mode 100644 index 00000000..21087ef6 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/CommentReportRepository.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.CommentReport; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CommentReportRepository extends JpaRepository { + Optional findByUserAndComment(User user, Comment comment); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/CommentRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/CommentRepository.java new file mode 100644 index 00000000..41827139 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/CommentRepository.java @@ -0,0 +1,20 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.*; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + Slice findByPostAndIdGreaterThanAndParentIsNullOrderByIdAsc(Post post, Long id, Pageable pageable); + + @Query("select c from CommentBlock cb join cb.user u join cb.comment c where u = :user and c.post.id = :postId") + List findByUserAndPostId(User user, Long postId); + + @Query("select c from CommentBlock cb join cb.comment c where cb.user = :user") + List findByBlockComment(User user); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/ExitReasonRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/ExitReasonRepository.java new file mode 100644 index 00000000..f50b90ec --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/ExitReasonRepository.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.ExitReason; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ExitReasonRepository extends JpaRepository { +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/IndividualPlanRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/IndividualPlanRepository.java new file mode 100644 index 00000000..cf090740 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/IndividualPlanRepository.java @@ -0,0 +1,19 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.IndividualPlan; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDateTime; +import java.util.List; + +public interface IndividualPlanRepository extends JpaRepository { + + List findByUserIdAndDateBetween(Long userID, LocalDateTime startDate, LocalDateTime endDate); + + Slice findByUserAndDateBetween(User user, LocalDateTime startOfDay, LocalDateTime endOfDay, Pageable pageable); + + int countByUserAndDateBetween(User user, LocalDateTime startOfDay, LocalDateTime endOfDay); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/MoimRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/MoimRepository.java new file mode 100644 index 00000000..d074d4a7 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/MoimRepository.java @@ -0,0 +1,38 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface MoimRepository extends JpaRepository { + + @Query("SELECT m FROM UserMoim um join um.moim m where um.user.id = :userId and um.joinStatus = 'COMPLETE' and m.id < :cursor order by m.id desc") + Slice findMyMoims(Long userId, Long cursor, Pageable pageable); + + @Query("SELECT m FROM Moim m WHERE m.id < :id AND m.moimCategory IN :moimCategories AND m.name LIKE %:name% order by m.id desc") + Slice findByMoimCategoryAndNameLikeAndIdLessThanOrderByIdDesc( + List moimCategories, + String name, + Long id, + Pageable pageable + ); + + + Slice findByNameLikeAndIdLessThanOrderByIdDesc(String name, Long id, Pageable pageable); + + Slice findByIdLessThanOrderByIdDesc(Long Id, Pageable pageable); + + @Query("select m from UserMoim um join um.moim m where um.user = :user and um.joinStatus = 'COMPLETE'") + List findMoimsByUser(User user); + + @Query("SELECT m FROM UserMoim um join um.moim m where um.user.id = :userId and um.joinStatus = 'COMPLETE' and um.moimRole = :moimRole and m.id < :cursor order by m.id desc") + Slice findMyMoimsWithMoimRole(Long userId, Long cursor, MoimRole moimRole, PageRequest of); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/PlanRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/PlanRepository.java new file mode 100644 index 00000000..ad3d26e4 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/PlanRepository.java @@ -0,0 +1,50 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.Plan; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface PlanRepository extends JpaRepository { + + List findByDateBetween(LocalDateTime startDate, LocalDateTime endDate); + + @Query("SELECT p FROM UserPlan up JOIN up.plan p WHERE up.user = :user AND p.date BETWEEN :startOfDay AND :endOfDay ORDER BY p.date ASC") + Slice findByUserAndDateBetween(User user, LocalDateTime startOfDay, LocalDateTime endOfDay, Pageable pageable); + + List findByMoim(Moim moim); + + @Query(value = "SELECT p.id, p.title, p.date, p.location, p.location_detail, NULL as memo, m.id as moimId, m.name as moimName, 'MOIM_PLAN' as plan_type " + + "FROM plan p " + + "JOIN moim m ON p.moim_id = m.id " + + "JOIN user_plan up ON p.id = up.plan_id " + + "WHERE up.user_id = :userId " + + "AND p.date BETWEEN :startOfDay AND :endOfDay " + + "UNION " + + "SELECT ip.id, ip.title, ip.date, ip.location, ip.location_detail, ip.memo, NULL as moimId, NULL as moimName, 'INDIVIDUAL_PLAN' as plan_type " + + "FROM individual_plan ip " + + "WHERE ip.user_id = :userId " + + "AND ip.date BETWEEN :startOfDay AND :endOfDay " + + "UNION " + + "SELECT t.id, t.title, t.due_date as date, NULL as location, NULL as location_detail, t.content as memo, m.id as moimId, m.name as moimName, 'TODO_PLAN' as plan_type " + + "FROM todo t " + + "JOIN user_todo ut ON t.id = ut.todo_id " + + "LEFT JOIN moim m ON t.moim_id = m.id " + + "WHERE ut.user_id = :userId " + + "AND t.due_date BETWEEN :startOfDay AND :endOfDay " + + "ORDER BY date ASC " + + "LIMIT :size OFFSET :offset", nativeQuery = true) + List findUserPlansAndIndividualPlans( + @Param("userId") Long userId, + @Param("startOfDay") LocalDateTime startOfDay, + @Param("endOfDay") LocalDateTime endOfDay, + @Param("size") int size, + @Param("offset") int offset); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/PostBlockRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/PostBlockRepository.java new file mode 100644 index 00000000..c04bb5eb --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/PostBlockRepository.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.PostBlock; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PostBlockRepository extends JpaRepository { + Optional findByUserIdAndPostId(Long userId, Long postId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/PostImageRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/PostImageRepository.java new file mode 100644 index 00000000..09ecc508 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/PostImageRepository.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.PostImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostImageRepository extends JpaRepository { +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/PostLikeRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/PostLikeRepository.java new file mode 100644 index 00000000..3236bcf2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/PostLikeRepository.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PostLikeRepository extends JpaRepository { + Boolean existsPostLikeByUserIdAndPostId(Long userId, Long postId); + Optional findByUserIdAndPostId(Long userId, Long postId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/PostReportRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/PostReportRepository.java new file mode 100644 index 00000000..b1ed0f3a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/PostReportRepository.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.PostReport; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PostReportRepository extends JpaRepository { + Optional findByUserAndPost(User user, Post post); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/PostRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/PostRepository.java new file mode 100644 index 00000000..4b6918c4 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/PostRepository.java @@ -0,0 +1,42 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.PostType; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface PostRepository extends JpaRepository { + @Query("SELECT p FROM Post p " + + "WHERE p.moim = :moim " + + "AND p.postType = :postType " + + "AND p.id < :id " + + "AND p NOT IN (SELECT pb.post FROM PostBlock pb WHERE pb.user = :user) " + + "ORDER BY p.id DESC") + Slice findByMoimAndPostTypeAndIdLessThanAndUserPostBlocksNotInOrderByIdDesc(Moim moim, PostType postType, Long id, User user, Pageable pageable); + + + @Query("SELECT p FROM Post p " + + "WHERE p.moim = :moim " + + "AND p.postType != 'GLOBAL' " + + "AND p.id < :id " + + "AND p NOT IN (SELECT pb.post FROM PostBlock pb WHERE pb.user = :user) " + + "ORDER BY p.id DESC") + Slice findByMoimAndIdLessThanAndUserPostBlocksNotInOrderByIdDesc(Moim moim, Long id, User user, Pageable pageable); + + List findByMoimAndPostType(Moim moim, PostType postType); + + Slice findByPostTypeAndIdLessThanOrderByIdDesc(PostType postType, Long id, Pageable pageable); + + @Query("select p from Post p join p.moim m where m = :moim and p.postType != :postType") + List findByNotPostTypeAndMoimOrderByCreatedAtDesc(PostType postType, Moim moim, Pageable pageable); + + @Query("select p from PostBlock pb join pb.post p where pb.user = :user") + List findBlockPost(User user); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/ReadPostRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/ReadPostRepository.java new file mode 100644 index 00000000..0ae35f62 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/ReadPostRepository.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.ReadPost; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ReadPostRepository extends JpaRepository { + Optional findByUserAndPost(User user, Post post); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/ScheduleRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/ScheduleRepository.java new file mode 100644 index 00000000..64de89e0 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/ScheduleRepository.java @@ -0,0 +1,11 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.Schedule; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ScheduleRepository extends JpaRepository { + + List findAllByPlanId(Long planId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/TodoImageRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/TodoImageRepository.java new file mode 100644 index 00000000..b86c5f9e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/TodoImageRepository.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.TodoImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TodoImageRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/moim/repository/TodoRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/TodoRepository.java new file mode 100644 index 00000000..813c3cb4 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/TodoRepository.java @@ -0,0 +1,29 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.enums.TodoStatus; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.time.LocalDateTime; +import java.util.List; + +public interface TodoRepository extends JpaRepository { + + boolean existsById(Long todoId); + + @Query("SELECT t FROM Todo t WHERE t.moim.id = :moimId AND t.id < :cursor ORDER BY t.id DESC") + Slice findByMoimIdAndCursorLessThan(Long moimId, Long cursor, Pageable pageable); + + @Query("SELECT t FROM Todo t WHERE t.writer.id = :writerId AND t.moim.id = :moimId AND t.id < :cursor ORDER BY t.id DESC") + Slice findByWriterIdAndMoimIdAndCursorLessThan(Long writerId, Long moimId, Long cursor, Pageable pageable); + + @Query("SELECT t FROM Todo t WHERE t.writer.id = :writerId AND t.id < :cursor ORDER BY t.id DESC") + Slice findByWriterIdAndCursorLessThan(Long writerId, Long cursor, Pageable pageable); + + List findAllByStatusAndDueDateBefore(TodoStatus todoStatus, LocalDateTime now); + + List findAllByDueDateBetween(LocalDateTime tomorrow, LocalDateTime endOfTomorrow); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/UserMoimRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/UserMoimRepository.java new file mode 100644 index 00000000..a26abd66 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/UserMoimRepository.java @@ -0,0 +1,118 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.Post; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.service.impl.dto.IntroduceVideoDTO; +import com.dev.moim.domain.moim.service.impl.dto.JoinRequestDTO; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface UserMoimRepository extends JpaRepository { + + Boolean existsByUserIdAndMoimIdAndJoinStatus(Long userId, Long moimId, JoinStatus joinStatus); + + @Query("select um from UserMoim um where um.user.id = :userId and um.moim.id = :moimId and um.joinStatus = :joinStatus") + Optional findByUserIdAndMoimId(Long userId, Long moimId, JoinStatus joinStatus); + + @Query("select um from UserMoim um where um.moim.id = :moimId and um.joinStatus = :joinStatus") + List findByMoimId(Long moimId, JoinStatus joinStatus); + + @Query("select um from UserMoim um where um.user = :user and um.moim = :moim and um.joinStatus = 'COMPLETE'") + Optional findByUserAndMoim(User user, Moim moim); + + Boolean existsByUserAndMoim(User user, Moim moim); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.IntroduceVideoDTO(m, up) from UserMoim um join um.userProfile up join um.moim m where um.moim.id = :moimId and um.moimRole = 'OWNER'") + Optional findIntroduceVideo(Long moimId); + + List findByUserId(Long userId); + + Optional findProfileIdByUserAndMoim(User user, Moim moim); + + @Query("SELECT um " + + "FROM UserMoim um " + + "WHERE um.user = :user " + + "AND um.moim = :moim " + + "AND um.joinStatus IN :joinStatuses") + List findByUserAndMoimAndJoinRequest(User user, Moim moim, List joinStatuses); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.JoinRequestDTO(m, um) from UserMoim um join um.moim m where um.user = :user and um.confirm = false and um.id < :cursor order by um.id desc") + Slice findMyRequestMoims(User user, Long cursor, Pageable pageable); + + Optional findByMoimIdAndMoimRole(Long moimId, MoimRole moimRole); + + boolean existsByUserAndMoimRole(User user, MoimRole moimRole); + + @Query("SELECT um.joinStatus FROM UserMoim um " + + "WHERE um.user = :user AND um.moim = :moim AND um.joinStatus != 'REJECT'") + JoinStatus findJoinStatusByUserAndMoim(User user, Moim moim); + + @Query("select um.moimRole from UserMoim um where um.user = :user and um.moim = :moim and um.joinStatus = 'COMPLETE'") + Optional findMoimRoleByUserAndMoim(User user, Moim moim); + + @Query("select um from Comment c join c.userMoim um where c = :comment") + Optional findByComment(Comment comment); + + @Query("select um from Post p join p.userMoim um where p = :post") + Optional findByPost(Post post); + + int countByUserIdAndJoinStatus(Long userId, JoinStatus joinStatus); + + List findByMoimIdAndJoinStatus(Long moimId, JoinStatus joinStatus); + + @Query("SELECT um FROM UserMoim um WHERE um.moim.id = :moimId AND um.user.id IN :userIds") + List findByMoimIdAndUserIds(@Param("moimId") Long moimId, @Param("userIds") Set userIds); + + @Query("SELECT um FROM UserMoim um " + + "LEFT JOIN UserTodo ut ON um.user.id = ut.user.id AND ut.todo.id = :todoId " + + "WHERE um.moim.id = :moimId AND ut.id IS NULL " + + "AND um.joinStatus = :joinStatus " + + "AND um.id > :cursor " + + "ORDER BY um.id ASC") + Slice findAllMembersNotAssignedToTodo( + @Param("moimId") Long moimId, + @Param("todoId") Long todoId, + @Param("joinStatus") JoinStatus joinStatus, + @Param("cursor") Long cursor, + Pageable pageable); + + @Query("select new com.dev.moim.domain.moim.service.impl.dto.JoinRequestDTO(m, um) from UserMoim um join um.moim m where um.user = :user and um.confirm = false and um.joinStatus = :joinStatus and um.id < :cursor order by um.id desc") + Slice findMyRequestMoimsWithJoinStatus(User user, Long cursor, JoinStatus joinStatus, PageRequest of); + + @Modifying + @Query("delete from UserMoim um where um.confirm = true and um.joinStatus not in :joinStatusList") + void deleteAllByConfirmUserMoim(List joinStatusList); + + @Query("SELECT um FROM UserMoim um WHERE um.user.id = :userId AND um.moim.id IN :moimIdList") + List findAllByUserIdAndMoimIdList(Long userId, List moimIdList); + + @Query("SELECT um FROM UserMoim um WHERE um.user.id = :userId AND um.moim.id IN :moimIdList AND um.joinStatus = :joinStatus") + List findAllByUserIdAndMoimIdListAndJoinStatus(Long userId, List moimIdList, JoinStatus joinStatus); + + boolean existsByUserProfileIdAndJoinStatus(Long profileId, JoinStatus joinStatus); + + @Query("SELECT um FROM UserMoim um " + + "JOIN FETCH um.moim " + + "WHERE um.userProfile.id = :userProfileId AND um.joinStatus = :joinStatus " + + "AND um.id > :cursor " + + "ORDER BY um.id ASC") + Slice findAllByUserProfileIdAndJoinStatus( + @Param("userProfileId") Long userProfileId, + @Param("joinStatus") JoinStatus joinStatus, + @Param("cursor") Long cursor, + Pageable pageable); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/UserPlanRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/UserPlanRepository.java new file mode 100644 index 00000000..2dadbe64 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/UserPlanRepository.java @@ -0,0 +1,52 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.UserPlan; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface UserPlanRepository extends JpaRepository { + + Boolean existsByPlanIdAndUserId(Long planId, Long userId); + + @Query("SELECT COUNT(DISTINCT u.id) " + + "FROM User u " + + "LEFT JOIN UserPlan up ON u.id = up.user.id " + + "LEFT JOIN Plan p ON up.plan.id = p.id " + + "LEFT JOIN IndividualPlan ip ON u.id = ip.user.id " + + "WHERE (p.date BETWEEN :startDate AND :endDate AND up.user.id IN " + + "(SELECT um.user.id FROM UserMoim um WHERE um.moim.id = :moimId)) " + + "OR (ip.date BETWEEN :startDate AND :endDate AND u.id IN " + + "(SELECT um.user.id FROM UserMoim um WHERE um.moim.id = :moimId))") + int countUsersWithPlansInDateRange(@Param("moimId") Long moimId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); + + long countByPlanId(Long planId); + + Boolean existsByUserIdAndPlanId(Long userId, Long planId); + + List findByUserIdAndPlanDateBetween(Long userId, LocalDateTime startDate, LocalDateTime endDate); + + @Query("SELECT up FROM UserPlan up " + + "JOIN FETCH up.user u " + + "JOIN FETCH UserMoim um ON u.id = um.user.id AND um.moim.id = :moimId " + + "WHERE up.plan.id = :planId") + Slice findByPlanIdWithUserAndUserMoim(@Param("planId") Long planId, + @Param("moimId") Long moimId, + Pageable pageable); + + Optional findByUserIdAndPlanId(Long userId, Long planId); + + @Query("SELECT COUNT(up) FROM UserPlan up WHERE up.user = :user AND up.plan.date BETWEEN :startOfDay AND :endOfDay") + int countPlansByUserAndDateBetween(@Param("user") User user, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay); + + List findByPlanId(Long planId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/repository/UserTodoRepository.java b/src/main/java/com/dev/moim/domain/moim/repository/UserTodoRepository.java new file mode 100644 index 00000000..0c9c95a0 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/repository/UserTodoRepository.java @@ -0,0 +1,73 @@ +package com.dev.moim.domain.moim.repository; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.UserTodo; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface UserTodoRepository extends JpaRepository { + + Optional findByUserIdAndTodoId(Long userId, Long todoId); + + boolean existsByUserIdAndTodoId(Long userId, Long todoId); + + @Query("SELECT ut FROM UserTodo ut " + + "JOIN FETCH ut.user u " + + "JOIN FETCH u.userMoimList um " + + "JOIN FETCH um.userProfile up " + + "WHERE ut.todo.id = :todoId " + + "AND um.moim.id = ut.todo.moim.id " + + "AND ut.id > :cursor " + + "ORDER BY ut.id ASC") + Slice findAllWithUserMoimAndUserProfileByTodoIdAndCursor( + @Param("todoId") Long todoId, + @Param("cursor") Long cursor, + Pageable pageable); + + @Query("SELECT COUNT(ut) " + + "FROM UserTodo ut " + + "JOIN ut.todo t " + + "WHERE ut.user = :user " + + "AND t.dueDate BETWEEN :startOfDay AND :endOfDay") + int countByUserAndTodoDueDateBetween(@Param("user") User user, + @Param("startOfDay") LocalDateTime startOfDay, + @Param("endOfDay") LocalDateTime endOfDay); + + List findAllByTodoId(Long todoId); + + @Query("SELECT ut FROM UserTodo ut " + + "JOIN ut.todo t " + + "WHERE ut.user.id = :userId " + + "AND t.dueDate BETWEEN :startDate AND :endDate " + + "ORDER BY t.dueDate ASC") + List findByUserIdAndTodoDueDateBetween( + @Param("userId") Long userId, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); + + List findAllByTodoIdAndStatusNot(Long todoId, TodoAssigneeStatus todoAssigneeStatus); + + @Query("SELECT ut " + + "FROM UserTodo ut " + + "JOIN FETCH ut.todo t " + + "JOIN FETCH t.moim m " + + "WHERE ut.user.id = :userId " + + "AND m.id = :moimId " + + "AND ut.id < :cursor " + + "ORDER BY ut.id DESC") + Slice findUserTodosByUserIdAndMoimId( + @Param("userId") Long userId, + @Param("moimId") Long moimId, + @Param("cursor") Long cursor, + Pageable pageable + ); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/CalenderCommandService.java b/src/main/java/com/dev/moim/domain/moim/service/CalenderCommandService.java new file mode 100644 index 00000000..a409581e --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/CalenderCommandService.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.calender.PlanCreateDTO; + +public interface CalenderCommandService { + + Long createPlan(User user, Long moimId, PlanCreateDTO request); + + Long joinPlan(User user, Long moimId, Long planId); + + void cancelPlanParticipation(User user, Long moidId, Long planId); + + void updatePlan(User user, Long moimId, Long planId, PlanCreateDTO request); + + void deletePlan(User user, Long moimId, Long planId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/CalenderQueryService.java b/src/main/java/com/dev/moim/domain/moim/service/CalenderQueryService.java new file mode 100644 index 00000000..4d476b8b --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/CalenderQueryService.java @@ -0,0 +1,21 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.calender.*; + +public interface CalenderQueryService { + + PlanMonthListDTO getMoimPlans(User user, Long moimId, int year, int month); + + PlanDetailDTO getPlanDetails(User user, Long planId); + + ScheduleListDTO getSchedules(Long moiId, Long planId); + + PlanParticipantListPageDTO getPlanParticipants(Long moimId, Long planId, int page, int size); + + boolean existsByUserIdAndPlanId(Long userId,Long planId); + + Long findPlanWriter(Long planId); + + boolean existsByPlanId(Long planId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/MoimCommandService.java b/src/main/java/com/dev/moim/domain/moim/service/MoimCommandService.java new file mode 100644 index 00000000..4944fa3b --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/MoimCommandService.java @@ -0,0 +1,31 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.*; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import jakarta.validation.Valid; + +public interface MoimCommandService { + Moim createMoim(User user, CreateMoimDTO createMoimDTO); + + void withDrawMoim(User user, @Valid WithMoimDTO withMoimDTO); + + void modifyMoimInfo(@Valid UpdateMoimDTO updateMoimDTO); + + void joinMoim(User user, Long moimId); + + void acceptMoim(User user, MoimJoinConfirmRequestDTO moimJoinConfirmRequestDTO); + + ChangeAuthorityResponseDTO changeMemberAuthorities(User user, ChangeAuthorityRequestDTO changeAuthorityRequestDTO); + + void rejectMoims(MoimJoinConfirmRequestDTO moimJoinConfirmRequestDTO); + + void changeMoimLeader(User user, @Valid ChangeMoimLeaderRequestDTO changeMoimLeaderRequestDTO); + + void findMyRequestMoimsConfirm(User user, Long moimId); + + void moimExpel(User user, Long userId, Long moimId); + + MoimRoleResponse moimsMyRole(User user, Long moimId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/MoimQueryService.java b/src/main/java/com/dev/moim/domain/moim/service/MoimQueryService.java new file mode 100644 index 00000000..87cbb00f --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/MoimQueryService.java @@ -0,0 +1,46 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.controller.enums.MoimRequestJoin; +import com.dev.moim.domain.moim.controller.enums.MoimRequestRole; +import com.dev.moim.domain.moim.controller.enums.MoimRequestType; +import com.dev.moim.domain.moim.dto.MoimDetailDTO; +import com.dev.moim.domain.moim.dto.MoimIntroduceDTO; +import com.dev.moim.domain.moim.dto.MoimJoinRequestListDTO; +import com.dev.moim.domain.moim.dto.MoimPreviewListDTO; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.user.dto.UserPreviewListDTO; + +import java.util.List; + +public interface MoimQueryService { + + MoimPreviewListDTO getUserMoim(Long userId, Long cursor, Integer take, MoimRequestRole moimRequestRole); + + MoimPreviewListDTO findMoims(List moimRequestTypes, String name, Long cursor, Integer take); + + UserPreviewListDTO getMoimMembers(Long moimId, Long cursor, Integer take, String search); + + UserPreviewListDTO getMoimMembersExcludeOwner(Long moimId, Long cursor, Integer take, String search); + + UserPreviewListDTO findRequestMember(User user, Long moimId, Long cursor, Integer take, String search); + + MoimIntroduceDTO getIntroduce(Long moimId); + + MoimPreviewListDTO getPopularMoim(); + + MoimPreviewListDTO getNewMoim(Long cursor, Integer take); + + MoimDetailDTO getMoimDetail(User user, Long moimId); + + Long findMoimOwner(Long moimId); + + MoimJoinRequestListDTO findMyRequestMoims(User user, Long cursor, Integer take, MoimRequestJoin moimRequestJoin); + + boolean existsByMoimId(Long moimId); + + boolean existsByUserIdAndMoimIdAndJoinStatus(Long userId, Long moimId, JoinStatus joinStatus); + + List findAllMemberIdByMoimId(Long moimId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/PostCommandService.java b/src/main/java/com/dev/moim/domain/moim/service/PostCommandService.java new file mode 100644 index 00000000..415f2d82 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/PostCommandService.java @@ -0,0 +1,39 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.post.*; +import com.dev.moim.domain.moim.entity.Comment; +import com.dev.moim.domain.moim.entity.Post; +import jakarta.validation.Valid; + +public interface PostCommandService { + Post createMoimPost(User user, CreateMoimPostDTO createMoimPostDTO); + + Comment createComment(User user, CreateCommentDTO createCommentDTO); + + Comment createCommentComment(User user, CreateCommentCommentDTO createCommentCommentDTO); + + void postLike(User user, PostLikeDTO postLikeDTO); + + void commentLike(User user, CommentLikeDTO commentLikeDTO); + + void reportMoimPost(User user, PostReportDTO postReportDTO); + + void deletePost(User user, Long postId); + + Post updatePost(User user, @Valid UpdateMoimPostDTO updateMoimPostDTO); + + void blockPost(User user, @Valid PostBlockDTO postBlockDTO); + + void deleteComment(User user, Long commentId); + + void updateComment(User user, CommentUpdateRequestDTO commentUpdateRequestDTO); + + void reportComment(User user, CommentReportDTO commentReportDTO); + + void blockComment(User user, CommentBlockDTO commentBlockDTO); + + Long createAnnouncement(User user, @Valid AnnouncementRequestDTO announcementRequestDTO); + + void announcementConfirm(User user, @Valid AnnouncementConfirmRequestDTO announcementRequestDTO); +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/moim/service/PostQueryService.java b/src/main/java/com/dev/moim/domain/moim/service/PostQueryService.java new file mode 100644 index 00000000..6b393250 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/PostQueryService.java @@ -0,0 +1,31 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.controller.enums.PostRequestType; +import com.dev.moim.domain.moim.dto.post.*; +import com.dev.moim.domain.moim.entity.Post; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public interface PostQueryService { + MoimPostPreviewListDTO getMoimPostList(User user, Long moimId, PostRequestType postRequestType, Long cursor, Integer take); + + MoimPostDetailDTO getMoimPost(User user, Long moimId, Long postId); + + Boolean isCommentLike(Long userId, Long commentId); + + CommentResponseListDTO getcomments(User user, Long moimId, Long postId, Long cursor, Integer take); + + Boolean isPostLike(Long userId, Long postId); + + MoimPostPreviewListDTO getIntroductionPosts(Long cursor, Integer take); + + Post getIntroductionPost(Long postId); + + List getPostsByJoinMoims(User user); + + List findBlockComments(User user); + + List findBlockPosts(User user); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/TodoCommandService.java b/src/main/java/com/dev/moim/domain/moim/service/TodoCommandService.java new file mode 100644 index 00000000..5ed37e51 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/TodoCommandService.java @@ -0,0 +1,21 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.todo.*; + +public interface TodoCommandService { + + Long createTodo(User user, Long moimId, CreateTodoDTO request); + + UpdateTodoStatusResponseDTO updateUserTodoStatus(User user, Long todoId, UpdateTodoStatusDTO request); + + void updateTodo(User user, Long moimId, Long todoId, UpdateTodoDTO request); + + void deleteTodo(Long todoId); + + void addAssignees(User user, AddTodoAssigneeDTO request); + + void deleteAssignees(DeleteTodoAssigneeDTO request); + + void updateExpiredTodosAndAssigneesStatus(); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/TodoQueryService.java b/src/main/java/com/dev/moim/domain/moim/service/TodoQueryService.java new file mode 100644 index 00000000..ffe46bbc --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/TodoQueryService.java @@ -0,0 +1,39 @@ +package com.dev.moim.domain.moim.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.todo.TodoDetailDTO; +import com.dev.moim.domain.moim.dto.todo.TodoPageDTO; +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.UserTodo; + +import java.util.List; +import java.util.Optional; + +public interface TodoQueryService { + + TodoDetailDTO getTotalDetailForAssignee(User user, Long todoId); + + TodoDetailDTO getTodoDetailForAdmin(Long todoId); + + TodoPageDTO getTodoAssigneeListForAdmin(Long todoId, Long cursor, Integer take); + + TodoPageDTO getTodoNonAssigneeListForAdmin(Long moidId, Long todoId, Long cursor, Integer take); + + TodoPageDTO getMoimTodoListForAdmin(Long moimId, Long cursor, Integer take); + + TodoPageDTO getSpecificMoimTodoListByMe(User user, Long moimId, Long cursor, Integer take); + + TodoPageDTO getAssignedTodoListForUserInSpecificMoim(User user, Long moimId, Long cursor, Integer take); + + TodoPageDTO getTodoListByMe(User user, Long cursor, Integer take); + + boolean existsByUserIdAndTodoId(Long userId, Long todoId); + + boolean existsByTodoId(Long todoId); + + Optional findByUserIdAndTodoId(Long userId, Long todoId); + + Optional findTodoByTodoId(Long todoId); + + List findAssigneeByTodoId(Long todoId); +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/CalenderCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/CalenderCommandServiceImpl.java new file mode 100644 index 00000000..472dfec0 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/CalenderCommandServiceImpl.java @@ -0,0 +1,167 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.account.repository.UserProfileRepository; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.moim.dto.calender.PlanCreateDTO; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.Plan; +import com.dev.moim.domain.moim.entity.Schedule; +import com.dev.moim.domain.moim.entity.UserPlan; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.CalenderCommandService; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.PlanException; +import com.dev.moim.global.error.handler.UserException; +import com.dev.moim.global.firebase.service.FcmService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional +public class CalenderCommandServiceImpl implements CalenderCommandService { + + private final PlanRepository planRepository; + private final UserPlanRepository userPlanRepository; + private final MoimRepository moimRepository; + private final AlarmService alarmService; + private final FcmService fcmService; + private final UserProfileRepository userProfileRepository; + + @Override + public Long createPlan(User user, Long moimId, PlanCreateDTO request) { + + Moim moim = moimRepository.findById(moimId) + .orElseThrow(() -> new MoimException(MOIM_NOT_FOUND)); + + Plan plan = Plan.builder() + .title(request.title()) + .date(LocalDateTime.of(request.date(), request.startTime())) + .location(request.location()) + .locationDetail(request.locationDetail()) + .cost(request.cost()) + .scheduleList(new ArrayList<>()) + .user(user) + .moim(moim) + .build(); + + if (request.schedules() != null && !request.schedules().isEmpty()) { + request.schedules().forEach(scheduleDTO -> { + Schedule schedule = Schedule.builder() + .title(scheduleDTO.title()) + .startTime(scheduleDTO.startTime()) + .build(); + plan.addSchedule(schedule); + }); + } + + return planRepository.save(plan).getId(); + } + + @Override + public Long joinPlan(User user, Long moimId, Long planId) { + + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new PlanException(PLAN_NOT_FOUND)); + + UserPlan userPlan = UserPlan.builder() + .user(user) + .plan(plan) + .build(); + + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), ProfileType.MAIN) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + Optional.of(plan.getUser()) + .filter(planUser -> !user.equals(planUser)) + .ifPresent(planUser -> { + alarmService.saveAlarm(user, planUser, "[" + plan.getMoim().getName() + "]" + plan.getTitle(), userProfile.getName() + " 님이 참여 신청했습니다", AlarmType.PUSH, AlarmDetailType.PLAN, plan.getMoim().getId(), null, null); + + if (planUser.getIsPushAlarm() && planUser.getDeviceId() != null) { + fcmService.sendPushNotification(planUser, "[" + plan.getMoim().getName() + "]" + plan.getTitle(), userProfile.getName() + " 님이 참여 신청했습니다", AlarmDetailType.PLAN); + } + }); + + return userPlanRepository.save(userPlan).getId(); + } + + @Override + public void cancelPlanParticipation(User user, Long moidId, Long planId) { + + UserPlan userPlan = userPlanRepository.findByUserIdAndPlanId(user.getId(), planId) + .orElseThrow(() -> new PlanException(USER_NOT_PART_OF_PLAN)); + + userPlanRepository.delete(userPlan); + } + + @Override + public void updatePlan(User user, Long moimId, Long planId, PlanCreateDTO request) { + + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new PlanException(PLAN_NOT_FOUND)); + + List scheduleList = request.schedules().stream() + .map(schedule -> Schedule.builder() + .title(schedule.title()) + .startTime(schedule.startTime()) + .plan(plan) + .build()) + .toList(); + + plan.updatePlan( + request.title(), + LocalDateTime.of(request.date(), request.startTime()), + request.location(), + request.locationDetail(), + request.cost() + ); + + List participantList = userPlanRepository.findByPlanId(planId).stream().map(UserPlan::getUser).toList(); + + participantList.stream().filter(participant -> !user.equals(participant)) + .forEach(participant -> { + alarmService.saveAlarm(plan.getUser(), participant, "[" + plan.getMoim().getName() + "]" + plan.getTitle(), "일정이 수정되었습니다. 변경사항을 확인해주세요.", AlarmType.PUSH, AlarmDetailType.PLAN, plan.getMoim().getId(), null, null); + + if (participant.getIsPushAlarm() && participant.getDeviceId() != null) { + fcmService.sendPushNotification(participant, "[" + plan.getMoim().getName() + "]" + plan.getTitle(), "일정이 수정되었습니다. 변경사항을 확인해주세요.", AlarmDetailType.PLAN); + } + + }); + + plan.updateSchedule(scheduleList); + } + + @Override + public void deletePlan(User user, Long moimId, Long planId) { + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new PlanException(PLAN_NOT_FOUND)); + + List participantList = userPlanRepository.findByPlanId(planId).stream().map(UserPlan::getUser).toList(); + + participantList.stream().filter(participant -> !user.equals(participant)) + .forEach(participant -> { + alarmService.saveAlarm(plan.getUser(), participant, "[" + plan.getMoim().getName() + "]" + plan.getTitle(), "일정이 취소되었습니다.", AlarmType.PUSH, AlarmDetailType.PLAN, plan.getMoim().getId(), null, null); + + if (participant.getIsPushAlarm() && participant.getDeviceId() != null) { + fcmService.sendPushNotification(participant, "[" + plan.getMoim().getName() + "]" + plan.getTitle(), "일정이 취소되었습니다.", AlarmDetailType.PLAN); + } + }); + + planRepository.delete(plan); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/CalenderQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/CalenderQueryServiceImpl.java new file mode 100644 index 00000000..83686431 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/CalenderQueryServiceImpl.java @@ -0,0 +1,131 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.dto.calender.*; +import com.dev.moim.domain.moim.entity.Plan; +import com.dev.moim.domain.moim.entity.Schedule; +import com.dev.moim.domain.moim.entity.UserPlan; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.PlanException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.dev.moim.global.common.code.status.ErrorStatus.PLAN_NOT_FOUND; +import static com.dev.moim.global.common.code.status.ErrorStatus.PLAN_WRITER_NOT_FOUND; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class CalenderQueryServiceImpl implements CalenderQueryService { + + private final PlanRepository planRepository; + private final UserPlanRepository userPlanRepository; + private final ScheduleRepository scheduleRepository; + private final UserMoimRepository userMoimRepository; + + @Override + public PlanMonthListDTO getMoimPlans(User user, Long moimId, int year, int month) { + + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime startDate = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59); + + List monthPlanList = planRepository.findByDateBetween(startDate, endDate); + + Map> monthPlanListByDay = monthPlanList.stream() + .collect(Collectors.groupingBy(plan -> plan.getDate().getDayOfMonth())); + + Map planDayListMap = new HashMap<>(); + for (Map.Entry> entry : monthPlanListByDay.entrySet()) { + int day = entry.getKey(); + List dayPlans = entry.getValue(); + + LocalDateTime dayStart = YearMonth.of(year, month).atDay(day).atStartOfDay(); + LocalDateTime dayEnd = dayStart.plusDays(1).minusNanos(1); + + int memberWithPlanCnt = userPlanRepository.countUsersWithPlansInDateRange(moimId, dayStart, dayEnd); + + List planList = dayPlans.stream() + .filter(plan -> plan.getMoim().getId().equals(moimId)) + .map(plan -> MoimPlanDTO.from(plan, userPlanRepository.existsByPlanIdAndUserId(plan.getId(), user.getId()))) + .collect(Collectors.toList()); + + planDayListMap.put(day, new PlanDayListDTO(memberWithPlanCnt, planList)); + } + + return new PlanMonthListDTO<>(planDayListMap); + } + + @Override + public PlanDetailDTO getPlanDetails(User user, Long planId) { + + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new MoimException(PLAN_NOT_FOUND)); + + long participant = userPlanRepository.countByPlanId(planId); + + List scheduleList = scheduleRepository.findAllByPlanId(planId); + + Boolean isParticipant = userPlanRepository.existsByUserIdAndPlanId(user.getId(), planId); + + return PlanDetailDTO.from(plan, participant, scheduleList, isParticipant); + } + + @Override + public ScheduleListDTO getSchedules(Long moimId, Long planId) { + List scheduleList = scheduleRepository.findAllByPlanId(planId); + + return ScheduleListDTO.of(scheduleList); + } + + @Override + public PlanParticipantListPageDTO getPlanParticipants(Long moimId, Long planId, int page, int size) { + PageRequest pageRequest = PageRequest.of(page-1, size, Sort.by(Sort.Direction.ASC, "id")); + Slice userPlanPage = userPlanRepository.findByPlanIdWithUserAndUserMoim(planId, moimId, pageRequest); + + List userProfileList = userPlanPage.stream() + .map(UserPlan::getUser) + .map(user -> userMoimRepository.findByUserIdAndMoimId(user.getId(), moimId, JoinStatus.COMPLETE)) + .filter(Optional::isPresent) + .map(optionalUserMoim -> optionalUserMoim.get().getUserProfile()) + .toList(); + + return PlanParticipantListPageDTO.from(userProfileList, userPlanPage); + } + + @Override + public boolean existsByUserIdAndPlanId(Long userId, Long planId) { + return userPlanRepository.existsByUserIdAndPlanId(userId, planId); + } + + @Override + public Long findPlanWriter(Long planId) { + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new PlanException(PLAN_WRITER_NOT_FOUND)); + + return plan.getUser().getId(); + } + + @Override + public boolean existsByPlanId(Long planId) { + return planRepository.existsById(planId); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/MoimCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/MoimCommandServiceImpl.java new file mode 100644 index 00000000..b480bc37 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/MoimCommandServiceImpl.java @@ -0,0 +1,277 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.account.repository.UserProfileRepository; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.moim.dto.*; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.ExitReason; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.entity.enums.PostType; +import com.dev.moim.domain.moim.entity.enums.ProfileStatus; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.repository.ExitReasonRepository; +import com.dev.moim.domain.moim.repository.MoimRepository; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import com.dev.moim.domain.moim.service.MoimCommandService; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.UserException; +import com.dev.moim.global.firebase.service.FcmService; +import com.dev.moim.global.s3.service.S3Service; +import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.units.qual.C; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MoimCommandServiceImpl implements MoimCommandService { + + private final MoimRepository moimRepository; + private final UserMoimRepository userMoimRepository; + private final ExitReasonRepository exitReasonRepository; + private final UserProfileRepository userProfileRepository; + private final UserRepository userRepository; + private final S3Service s3Service; + private final FcmService fcmService; + private final AlarmService alarmService; + private final PostRepository postRepository; + + @Override + public Moim createMoim(User user, CreateMoimDTO createMoimDTO) { + + + Moim moim = Moim.builder() + .name(createMoimDTO.title()) + .introduction(createMoimDTO.introduction()) + .location(createMoimDTO.location()) + .moimCategory(createMoimDTO.moimCategory()) + .introduceVideoKeyName(imageNullProcess(createMoimDTO.introduceVideoKeyName())) + .introduceVideoTitle(createMoimDTO.introduceVideoTitle()) + .imageUrl(imageNullProcess(createMoimDTO.imageKeyName())) + .build(); + + moimRepository.save(moim); + + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), ProfileType.MAIN).orElseThrow(()-> new MoimException(ErrorStatus.USER_PROFILE_NOT_FOUND_MAIN)); + + + UserMoim userMoim = UserMoim.builder() + .moim(moim) + .user(user) + .moimRole(MoimRole.OWNER) + .joinStatus(JoinStatus.COMPLETE) + .profileStatus(ProfileStatus.PRIVATE) + .userProfile(userProfile) + .confirm(true) + .build(); + + userMoimRepository.save(userMoim); + + Post savedPost = Post.builder() + .title(createMoimDTO.title()) + .content(createMoimDTO.introduction()) + .postType(PostType.GLOBAL) + .userMoim(userMoim) + .moim(moim) + .build(); + + postRepository.save(savedPost); + + return moim; + } + + @Override + public void withDrawMoim(User user, WithMoimDTO withMoimDTO) { + Moim moim = moimRepository.findById(withMoimDTO.moimId()).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + ExitReason exitReason = ExitReason.builder().user(user).moim(moim).content(withMoimDTO.exitReason()).build(); + + exitReasonRepository.save(exitReason); + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + List users = userRepository.findUserByMoim(moim, JoinStatus.COMPLETE); + + if (users.size() > 1 && userMoim.getMoimRole().equals(MoimRole.OWNER)) { + throw new MoimException(ErrorStatus.OWNER_NOT_EXIT); + } + + userMoimRepository.delete(userMoim); + + Optional ownerByMoim = userRepository.findOwnerByMoim(moim); + if (ownerByMoim.isPresent()) { + UserMoim owner = ownerByMoim.get(); + if (owner.getUser().getIsPushAlarm() && (user != owner.getUser())) { + alarmService.saveAlarm(user, owner.getUser(), owner.getUserProfile().getName()+"님이 모임을 탈퇴하셨습니다.", owner.getUserProfile().getName()+"님이 모임을 탈퇴하셨습니다.", AlarmType.PUSH, AlarmDetailType.MOIM, moim.getId(), null, null); + fcmService.sendPushNotification(owner.getUser(), owner.getUserProfile().getName()+"님이 모임을 탈퇴하셨습니다.", owner.getUserProfile().getName()+"님이 모임을 탈퇴하셨습니다.", AlarmDetailType.MOIM); + } + } + + userMoimRepository.flush(); + + List byUserIdAndMoimId = userMoimRepository.findByMoimId(moim.getId(), JoinStatus.COMPLETE); + + if (byUserIdAndMoimId.isEmpty()) { + moimRepository.delete(moim); + } + } + + @Override + public void modifyMoimInfo(UpdateMoimDTO updateMoimDTO) { + Moim moim = moimRepository.findById(updateMoimDTO.moimId()).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + moim.updateMoim(updateMoimDTO.title(), updateMoimDTO.moimCategory(), updateMoimDTO.address(), updateMoimDTO.description(), imageNullProcess(updateMoimDTO.imageKeyName())); + } + + @Override + public synchronized void joinMoim(User user, Long moimId) { + Moim moim = moimRepository.findById(moimId).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + List userMoims = userMoimRepository.findByUserAndMoimAndJoinRequest(user, moim, List.of(JoinStatus.LOADING, JoinStatus.COMPLETE)); + + if (!userMoims.isEmpty()) { + throw new MoimException(ErrorStatus.ALREADY_REQUEST); + } + + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), ProfileType.MAIN).orElseThrow(()-> new MoimException(ErrorStatus.USER_PROFILE_NOT_FOUND_MAIN)); + UserMoim userMoim = UserMoim.builder() + .userProfile(userProfile) + .joinStatus(JoinStatus.LOADING) + .user(user) + .moimRole(MoimRole.MEMBER) + .moim(moim) + .profileStatus(ProfileStatus.PRIVATE) + .confirm(false) + .build(); + + Optional owner = userRepository.findOwnerByMoim(moim); + + if (owner.isPresent()) { + User realOwner = owner.get().getUser(); + if (realOwner.getIsPushAlarm() && user != realOwner) { + alarmService.saveAlarm(user, realOwner, "모임 가입 신청이 들어왔습니다.", "["+moim.getName()+"]에 모임 가입 신청이 들어왔습니다.", AlarmType.PUSH, AlarmDetailType.MOIM, moim.getId(), null, null); + fcmService.sendPushNotification(realOwner, "모임 가입 신청이 들어왔습니다.", "["+moim.getName()+"]에 모임 가입 신청이 들어왔습니다.", AlarmDetailType.MOIM); + } + } + + userMoimRepository.save(userMoim); + } + + @Override + public void acceptMoim(User owner, MoimJoinConfirmRequestDTO moimJoinConfirmRequestDTO) { + User user = userRepository.findById(moimJoinConfirmRequestDTO.userId()).orElseThrow(() -> new UserException(ErrorStatus.USER_NOT_FOUND)); + + + Moim moim = moimRepository.findById(moimJoinConfirmRequestDTO.moimId()).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.LOADING).orElseThrow(() -> new MoimException(ErrorStatus.NOT_REQUEST_JOIN)); + + userMoim.accept(); + userMoim.confirm(); + + + List admins = userRepository.findAdmins(moim); + + if (user.getIsPushAlarm() && user != owner) { + alarmService.saveAlarm(owner, user, moim.getName() + " 모임에 가입되었습니다", moim.getName() + "에 가입되었습니다", AlarmType.PUSH, AlarmDetailType.MOIM, moim.getId(), null, null); + fcmService.sendPushNotification(user, moim.getName() + " 모임에 가입되었습니다", moim.getName() + "에 가입되었습니다", AlarmDetailType.MOIM); + } + + admins.stream().filter(admin -> !admin.equals(owner) && admin.getIsPushAlarm()).forEach(admin -> { + alarmService.saveAlarm(owner, admin, moim.getName() + " 모임에 참여되었습니다", moim.getName() + "에 참여되었습니다", AlarmType.PUSH, AlarmDetailType.MOIM, moim.getId(), null, null); + fcmService.sendPushNotification(admin, moim.getName() + " 모임에 참여하었습니다", moim.getName() + "에 참여하었습니다", AlarmDetailType.MOIM); + }); + } + + @Override + public ChangeAuthorityResponseDTO changeMemberAuthorities(User user, ChangeAuthorityRequestDTO changeAuthorityRequestDTO) { + User targetUser = userRepository.findById(changeAuthorityRequestDTO.userId()).orElseThrow(() -> { + throw new UserException(ErrorStatus.USER_NOT_FOUND); + }); + Moim moim = moimRepository.findById(changeAuthorityRequestDTO.moimId()).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(targetUser.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + userMoim.changeStatus(changeAuthorityRequestDTO.moimRole()); + + return new ChangeAuthorityResponseDTO(targetUser.getId(), userMoim.getMoimRole()); + } + + @Override + public void rejectMoims(MoimJoinConfirmRequestDTO moimJoinConfirmRequestDTO) { + User user = userRepository.findById(moimJoinConfirmRequestDTO.userId()).orElseThrow(() -> new UserException(ErrorStatus.USER_NOT_FOUND)); + + Moim moim = moimRepository.findById(moimJoinConfirmRequestDTO.moimId()).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.LOADING).orElseThrow(() -> new MoimException(ErrorStatus.NOT_REQUEST_JOIN)); + + userMoim.reject(); + + Optional owner = userRepository.findByMoimAndMoimCategory(moim, MoimRole.OWNER); + + if (user.getIsPushAlarm() && owner.get() != user) { + alarmService.saveAlarm(owner.get(), user, moim.getName() + " 의 모임에 반려되셨습니다", moim.getName() + "에게 반려 되셨습니다.", AlarmType.PUSH, AlarmDetailType.MOIM, moim.getId(), null, null); + fcmService.sendPushNotification(user, moim.getName() + " 의 모임에 반려되셨습니다", moim.getName() + "에게 반려 되셨습니다.", AlarmDetailType.MOIM); + } + } + + @Override + public void changeMoimLeader(User owner, ChangeMoimLeaderRequestDTO changeMoimLeaderRequestDTO) { + Moim moim = moimRepository.findById(changeMoimLeaderRequestDTO.moimId()).orElseThrow(() -> new UserException(ErrorStatus.USER_NOT_FOUND)); + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(owner.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + userMoim.leaveOwner(); + + User target = userRepository.findById(changeMoimLeaderRequestDTO.userId()).orElseThrow(() -> new UserException(ErrorStatus.USER_NOT_FOUND)); + UserMoim targetUserMoim = userMoimRepository.findByUserIdAndMoimId(target.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + targetUserMoim.enterOwner(); + } + + @Override + public void findMyRequestMoimsConfirm(User user, Long userMoimId) { + UserMoim userMoim = userMoimRepository.findById(userMoimId).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + userMoim.confirm(); + } + + @Override + public void moimExpel(User user, Long userId, Long moimId) { + Moim moim = moimRepository.findById(moimId).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + UserMoim ownerMoim = userMoimRepository.findByUserAndMoim(user, moim).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + if (!ownerMoim.getMoimRole().equals(MoimRole.OWNER)) { + throw new MoimException(ErrorStatus.USER_NOT_MOIM_ADMIN); + } + + User target = userRepository.findById(userId).orElseThrow(() -> new UserException(ErrorStatus.USER_NOT_FOUND)); + + UserMoim targetMoim = userMoimRepository.findByUserAndMoim(target, moim).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + userMoimRepository.delete(targetMoim); + } + + @Override + public MoimRoleResponse moimsMyRole(User user, Long moimId) { + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moimId, JoinStatus.COMPLETE).orElseThrow(() -> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + return MoimRoleResponse.toMoimRoleResponse(userMoim.getMoimRole(), userMoim.getJoinStatus()); + } + + private String imageNullProcess(String imageKeyName) { + return imageKeyName == null || imageKeyName.isEmpty() || imageKeyName.isBlank() ? null : s3Service.generateStaticUrl(imageKeyName); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/MoimQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/MoimQueryServiceImpl.java new file mode 100644 index 00000000..ecc544cf --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/MoimQueryServiceImpl.java @@ -0,0 +1,302 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.Gender; +import com.dev.moim.domain.account.repository.UserProfileRepository; +import com.dev.moim.domain.moim.controller.enums.MoimRequestJoin; +import com.dev.moim.domain.moim.controller.enums.MoimRequestRole; +import com.dev.moim.domain.moim.dto.MoimDetailDTO; +import com.dev.moim.domain.moim.dto.MoimIntroduceDTO; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.Plan; +import com.dev.moim.domain.moim.dto.*; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.entity.enums.PostType; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.impl.dto.IntroduceVideoDTO; +import com.dev.moim.domain.moim.service.impl.dto.JoinRequestDTO; +import com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.moim.controller.enums.MoimRequestType; +import com.dev.moim.domain.moim.dto.MoimPreviewDTO; +import com.dev.moim.domain.moim.dto.MoimPreviewListDTO; +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.enums.MoimCategory; +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.domain.user.dto.UserPreviewDTO; +import com.dev.moim.domain.user.dto.UserPreviewListDTO; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.PlanException; +import com.dev.moim.global.s3.service.S3Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MoimQueryServiceImpl implements MoimQueryService { + + private final MoimRepository moimRepository; + private final UserRepository userRepository; + private final UserMoimRepository userMoimRepository; + private final PostRepository postRepository; + private final PlanRepository planRepository; + private final S3Service s3Service; + private final UserProfileRepository userProfileRepository; + + @Override + public MoimPreviewListDTO getUserMoim(Long userId, Long cursor, Integer take, MoimRequestRole moimRequestRole) { + + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + Slice myMoims; + if (moimRequestRole.equals(MoimRequestRole.ALL)) { + myMoims = moimRepository.findMyMoims(userId, cursor, PageRequest.of(0, take)); + } else { + MoimRole moimRole = MoimRole.valueOf(moimRequestRole.toString()); + myMoims = moimRepository.findMyMoimsWithMoimRole(userId, cursor, moimRole, PageRequest.of(0, take)); + } + + List findMyMoims = myMoims.stream().map((moim)->{ + return MoimPreviewDTO.toMoimPreviewDTO(moim, moim.getImageUrl()); + }).toList(); + + Long nextCursor = null; + if (!myMoims.isLast()) { + nextCursor = myMoims.toList().get(myMoims.toList().size() - 1).getId(); + } + + return MoimPreviewListDTO.toMoimPreviewListDTO(findMyMoims, nextCursor, myMoims.hasNext()); + } + + @Override + public MoimPreviewListDTO findMoims(List moimRequestTypes, String name, Long cursor, Integer take) { + + if (name == null) { + name = ""; + } + + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + Slice moimSlice; + if (moimRequestTypes.isEmpty()) { + moimSlice = moimRepository.findByNameLikeAndIdLessThanOrderByIdDesc(name, cursor, PageRequest.of(0, take)); + } else { + List moimCategory = moimRequestTypes.stream().map((moimRequestType -> MoimCategory.valueOf(moimRequestType.toString()))).toList(); + moimSlice = moimRepository.findByMoimCategoryAndNameLikeAndIdLessThanOrderByIdDesc(moimCategory, name, cursor, PageRequest.of(0, take)); + } + + Long nextCursor = null; + if (!moimSlice.isLast()) { + nextCursor = moimSlice.toList().get(moimSlice.toList().size() - 1).getId(); + } + + List moimPreviewDTOList = moimSlice.toList().stream().map((moim) -> { + return MoimPreviewDTO.toMoimPreviewDTO(moim, moim.getImageUrl()); + }).toList(); + + return MoimPreviewListDTO.toMoimPreviewListDTO(moimPreviewDTOList, nextCursor, moimSlice.hasNext()); + } + + @Override + public UserPreviewListDTO getMoimMembers(Long moimId, Long cursor, Integer take, String search) { + + Slice moimUsers = userRepository.findUserByMoimId(moimId, search, JoinStatus.COMPLETE, cursor, PageRequest.of(0, take)); + + List userPreviewDTOList = moimUsers.toList().stream().map(UserPreviewDTO::toUserPreviewDTO).toList(); + + Long nextCursor = null; + if (!moimUsers.isLast()) { + nextCursor = moimUsers.toList().get(moimUsers.toList().size() - 1).getUserMoim().getId(); + } + + return UserPreviewListDTO.toUserPreviewListDTO(userPreviewDTOList, moimUsers.hasNext(), nextCursor); + } + + @Override + public UserPreviewListDTO getMoimMembersExcludeOwner(Long moimId, Long cursor, Integer take, String search) { + + Slice moimUsers = userRepository.findUserByMoimIdExcludeOwner(moimId, search, JoinStatus.COMPLETE, cursor, PageRequest.of(0, take)); + + List userPreviewDTOList = moimUsers.toList().stream().map(UserPreviewDTO::toUserPreviewDTO).toList(); + + Long nextCursor = null; + if (!moimUsers.isLast()) { + nextCursor = moimUsers.toList().get(moimUsers.toList().size() - 1).getUserMoim().getId(); + } + + return UserPreviewListDTO.toUserPreviewListDTO(userPreviewDTOList, moimUsers.hasNext(), nextCursor); + } + + @Override + public UserPreviewListDTO findRequestMember(User user, Long moimId, Long cursor, Integer take, String search) { + + + Slice moimUsers = userRepository.findUserByMoimId(moimId, search, JoinStatus.LOADING, cursor, PageRequest.of(0, take)); + + List userPreviewDTOList = moimUsers.toList().stream().map(UserPreviewDTO::toUserPreviewDTO).toList(); + + Long nextCursor = null; + if (!moimUsers.isLast()) { + nextCursor = moimUsers.toList().get(moimUsers.toList().size() - 1).getUserMoim().getId(); + } + + return UserPreviewListDTO.toUserPreviewListDTO(userPreviewDTOList, moimUsers.hasNext(), nextCursor); + } + + @Override + public MoimIntroduceDTO getIntroduce(Long moimId) { + Moim moim = moimRepository.findById(moimId).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + IntroduceVideoDTO introduceVideo = userMoimRepository.findIntroduceVideo(moimId).orElseThrow(() -> new MoimException(ErrorStatus.VIDEO_ERROR)); + return MoimIntroduceDTO.toMoimIntroduceDTO(introduceVideo.getMoim(), introduceVideo.getUserProfile()); + } + + @Override + public MoimPreviewListDTO getPopularMoim() { + + return null; + } + + @Override + public MoimPreviewListDTO getNewMoim(Long cursor, Integer take) { + + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + Slice moims = moimRepository.findByIdLessThanOrderByIdDesc(cursor, PageRequest.of(0, take)); + + List findMyMoims = moims.stream().map((moim)->{ + return MoimPreviewDTO.toMoimPreviewDTO(moim, moim.getImageUrl()); + }).toList(); + + + Long nextCursor = null; + if (!moims.isLast()) { + nextCursor = moims.toList().get(moims.toList().size() - 1).getId(); + } + + return MoimPreviewListDTO.toMoimPreviewListDTO(findMyMoims, nextCursor, moims.hasNext()); + } + + @Override + public MoimDetailDTO getMoimDetail(User user, Long moimId) { + Moim moim = moimRepository.findById(moimId).orElseThrow(() -> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + int reviewCount = postRepository.findByMoimAndPostType(moim, PostType.REVIEW).size(); + List users = userRepository.findUserByMoim(moim, JoinStatus.COMPLETE); + List moims = planRepository.findByMoim(moim); + JoinStatus joinStatus = userMoimRepository.findJoinStatusByUserAndMoim(user, moim); + List userProfileList = userProfileRepository.findRandomProfile(moim, JoinStatus.COMPLETE, PageRequest.of(0, 3)); + List userPreviewDTOList = userProfileList.stream().map(UserPreviewDTO::toUserPreviewDTO).toList(); + Optional moimRoleByUser = userMoimRepository.findMoimRoleByUserAndMoim(user, moim); + + Double totalAge = 0.0; + Double averageAge = 0.0; + Long maleSize = 0L; + Long femaleSize = 0L; + Long nonSelectCount = 0L; + Long count = 0L; + + for (User u : users) { + Gender gender = u.getGender(); + if (Gender.MALE.equals(gender)) { + maleSize++; + } else if (Gender.FEMALE.equals(gender)) { + femaleSize++; + } else { + nonSelectCount++; + } + + LocalDate birthDate = u.getBirth(); + if (birthDate != null) { + totalAge += LocalDate.now().getYear() - birthDate.getYear(); + count++; + } + } + + if (count > 0) { + averageAge = totalAge / count; + } + + MoimRole moimRole = moimRoleByUser.orElse(null); + + return MoimDetailDTO.toMoimDetailDTO(moim, moimRole, joinStatus, moim.getImageUrl(), averageAge, moims.size(), reviewCount, maleSize, femaleSize, nonSelectCount, users.size(), userPreviewDTOList); + } + + @Override + public Long findMoimOwner(Long planId) { + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new PlanException(PLAN_NOT_FOUND)); + + UserMoim userMoim = userMoimRepository.findByMoimIdAndMoimRole(plan.getMoim().getId(), MoimRole.OWNER) + .orElseThrow(() -> new MoimException(MOIM_OWNER_NOT_FOUND)); + + return userMoim.getUser().getId(); + } + + @Override + public MoimJoinRequestListDTO findMyRequestMoims(User user, Long cursor, Integer take, MoimRequestJoin moimRequestJoin) { + + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + Slice joinRequestDTOSlice; + if (moimRequestJoin.equals(MoimRequestJoin.ALL)) { + joinRequestDTOSlice = userMoimRepository.findMyRequestMoims(user, cursor, PageRequest.of(0, take)); + } else { + JoinStatus joinStatus = JoinStatus.valueOf(moimRequestJoin.toString()); + joinRequestDTOSlice = userMoimRepository.findMyRequestMoimsWithJoinStatus(user, cursor, joinStatus, PageRequest.of(0, take)); + } + + + List moimJoinRequestDTOList = joinRequestDTOSlice.map((j) -> { + List users = userRepository.findUserByMoim(j.getMoim(), JoinStatus.COMPLETE); + return MoimJoinRequestDTO.toMoimJoinRequestDTO(j, users.size()); + } + ).toList(); + + Long nextCursor = null; + if (!joinRequestDTOSlice.isLast()) { + nextCursor = joinRequestDTOSlice.toList().get(joinRequestDTOSlice.toList().size() - 1).getUserMoim().getId(); + } + + return MoimJoinRequestListDTO.toMoimJoinRequestListDTO(moimJoinRequestDTOList, joinRequestDTOSlice.hasNext(), nextCursor); + } + + @Override + public boolean existsByMoimId(Long moimId) { + return moimRepository.existsById(moimId); + } + + @Override + public boolean existsByUserIdAndMoimIdAndJoinStatus(Long userId, Long moimId, JoinStatus joinStatus) { + return userMoimRepository.existsByUserIdAndMoimIdAndJoinStatus(userId, moimId, joinStatus); + } + + @Override + public List findAllMemberIdByMoimId(Long moimId) { + return userMoimRepository.findByMoimIdAndJoinStatus(moimId, JoinStatus.COMPLETE).stream() + .map(userMoim -> userMoim.getUser().getId()) + .toList(); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/PostCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/PostCommandServiceImpl.java new file mode 100644 index 00000000..bcf9fa10 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/PostCommandServiceImpl.java @@ -0,0 +1,411 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.repository.AlarmRepository; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.moim.dto.post.*; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.enums.CommentStatus; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.PostType; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.PostCommandService; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.CommentException; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.PostException; +import com.dev.moim.global.firebase.service.FcmService; +import com.dev.moim.global.s3.service.S3Service; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional +public class PostCommandServiceImpl implements PostCommandService { + + private final MoimRepository moimRepository; + private final UserMoimRepository userMoimRepository; + private final PostRepository postRepository; + private final PostImageRepository postImageRepository; + private final CommentRepository commentRepository; + private final PostLikeRepository postLikeRepository; + private final CommentLikeRepository commentLikeRepository; + private final PostReportRepository postReportRepository; + private final PostBlockRepository postBlockRepository; + private final FcmService fcmService; + private final CommentReportRepository commentReportRepository; + private final CommentBlockRepository commentBlockRepository; + private final S3Service s3Service; + private final UserRepository userRepository; + private final AlarmService alarmService; + private final ReadPostRepository readPostRepository; + + @Override + public Post createMoimPost(User user, CreateMoimPostDTO createMoimPostDTO) { + + if (createMoimPostDTO.postType().equals(PostType.ANNOUNCEMENT)) { + throw new PostException(ErrorStatus._FORBIDDEN); + } + + Moim moim = moimRepository.findById(createMoimPostDTO.moimId()).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(()-> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + Post savedPost = Post.builder() + .title(createMoimPostDTO.title()) + .content(createMoimPostDTO.content()) + .postType(createMoimPostDTO.postType()) + .userMoim(userMoim) + .moim(moim) + .build(); + + postRepository.save(savedPost); + + createMoimPostDTO.imageKeyNames().forEach((i) ->{ + PostImage postImage = PostImage.builder().imageKeyName(i == null || i.isEmpty() || i.isBlank() ? null : s3Service.generateStaticUrl(i)).post(savedPost).build(); + postImageRepository.save(postImage); + } + ); + + return savedPost; + } + + @Override + public Comment createComment(User user, CreateCommentDTO createCommentDTO) { + + Moim moim = moimRepository.findById(createCommentDTO.moimId()).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(()-> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + Post post = postRepository.findById(createCommentDTO.postId()).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + + Comment comment = Comment.builder() + .content(createCommentDTO.content()) + .post(post) + .commentStatus(CommentStatus.ACTIVE) + .userMoim(userMoim) + .build(); + + commentRepository.save(comment); + + if (post.getUserMoim().getUser().getIsPushAlarm() && post.getUserMoim().getUser() != user) { + alarmService.saveAlarm(user, post.getUserMoim().getUser(), "새로운 댓글이 달렸습니다.", comment.getContent(), AlarmType.PUSH, AlarmDetailType.COMMENT, moim.getId(), post.getId(), comment.getId()); + fcmService.sendPushNotification(post.getUserMoim().getUser(), "새로운 댓글이 달렸습니다.", comment.getContent(), AlarmDetailType.COMMENT); + } + + return comment; + } + + @Override + public Comment createCommentComment(User user, CreateCommentCommentDTO createCommentCommentDTO) { + Moim moim = moimRepository.findById(createCommentCommentDTO.moimId()).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(()-> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + Post post = postRepository.findById(createCommentCommentDTO.postId()).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + + Comment parentComment = commentRepository.findById(createCommentCommentDTO.commentId()).orElseThrow(()-> new CommentException(ErrorStatus.COMMENT_NOT_FOUND)); + + Comment comment = Comment.builder() + .parent(parentComment) + .content(createCommentCommentDTO.content()) + .post(post) + .commentStatus(CommentStatus.ACTIVE) + .userMoim(userMoim) + .build(); + + commentRepository.save(comment); + + if (parentComment.getUserMoim().getUser().getIsPushAlarm() && parentComment.getUserMoim().getUser() != user) { + alarmService.saveAlarm(user, parentComment.getUserMoim().getUser(), "새로운 대댓글이 달렸습니다.", comment.getContent(), AlarmType.PUSH, AlarmDetailType.COMMENT, moim.getId(), post.getId(), comment.getId()); + fcmService.sendPushNotification(parentComment.getUserMoim().getUser(), "새로운 대댓글이 달렸습니다.", comment.getContent(), AlarmDetailType.COMMENT); + } + + return comment; + } + + @Override + public void postLike(User user, PostLikeDTO postLikeDTO) { + Post post = postRepository.findById(postLikeDTO.postId()).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + + Optional postLike = postLikeRepository.findByUserIdAndPostId(user.getId(), postLikeDTO.postId()); + + if (postLike.isPresent()) { + postLikeRepository.delete(postLike.get()); + } else { + PostLike savedPostLike = PostLike.builder() + .user(user) + .post(post) + .build(); + + postLikeRepository.save(savedPostLike); + + Optional postByUserMoim = userMoimRepository.findByPost(post); + + if (postByUserMoim.isPresent() && post.getUserMoim().getUser().getIsPushAlarm() && post.getUserMoim().getUser() != user) { + alarmService.saveAlarm(user, post.getUserMoim().getUser(), "좋아요가 달렸습니다", post.getTitle()+"에 좋아요가 달렸습니다", AlarmType.PUSH, AlarmDetailType.POST, post.getMoim().getId(), post.getId(), null); + fcmService.sendPushNotification(post.getUserMoim().getUser(), "좋아요가 달렸습니다.", post.getTitle()+"에 좋아요가 달렸습니다", AlarmDetailType.POST); + } + } + } + + @Override + public void commentLike(User user, CommentLikeDTO commentLikeDTO) { + Comment comment = commentRepository.findById(commentLikeDTO.commentId()).orElseThrow(()-> new CommentException(ErrorStatus.COMMENT_NOT_FOUND)); + + Optional commentLike = commentLikeRepository.findCommentLikeByUserIdAndCommentId(user.getId(), commentLikeDTO.commentId()); + + if (commentLike.isPresent()) { + commentLikeRepository.delete(commentLike.get()); + } else { + CommentLike savedCommentLike = CommentLike.builder() + .comment(comment) + .user(user) + .build(); + + commentLikeRepository.save(savedCommentLike); + + Optional userMoimByComment = userMoimRepository.findByComment(comment); + + if (userMoimByComment.isPresent() && comment.getUserMoim().getUser().getIsPushAlarm() && comment.getUserMoim().getUser() != user) { + alarmService.saveAlarm(user, comment.getUserMoim().getUser(), "좋아요가 달렸습니다", comment.getContent()+"에 좋아요가 달렸습니다", AlarmType.PUSH, AlarmDetailType.COMMENT, comment.getUserMoim().getMoim().getId(), comment.getId(), comment.getId()); + fcmService.sendPushNotification(comment.getUserMoim().getUser(), "좋아요가 달렸습니다.", comment.getContent()+"에 좋아요가 달렸습니다", AlarmDetailType.COMMENT); + } + } + } + + @Override + public void reportMoimPost(User user, PostReportDTO postReportDTO) { + + Post post = postRepository.findById(postReportDTO.postId()).orElseThrow(() -> new PostException(ErrorStatus.POST_NOT_FOUND)); + + Optional postReport = postReportRepository.findByUserAndPost(user, post); + + if (postReport.isPresent()) { + + // 이미 있음. + postReportRepository.delete(postReport.get()); + } else { + + // 없음 -> 삭제 + PostReport savedPostReport = PostReport.builder() + .post(post) + .user(user) + .build(); + + postReportRepository.save(savedPostReport); + } + + + } + + @Override + public void deletePost(User user, Long postId) { + Post post = postRepository.findById(postId).orElseThrow(() -> new PostException(ErrorStatus.POST_NOT_FOUND)); + + postRepository.delete(post); + } + + @Override + public Post updatePost(User user, UpdateMoimPostDTO updateMoimPostDTO) { + Post updatePost = postRepository.findById(updateMoimPostDTO.postId()) + .orElseThrow(() -> new PostException(ErrorStatus.POST_NOT_FOUND)); + + // imageKeyNames()의 반환값이 null인지 체크 + List imageKeyNames = updateMoimPostDTO.imageKeyNames(); + if (imageKeyNames == null) { + imageKeyNames = Collections.emptyList(); // 빈 리스트로 초기화 + } + + List imageList = imageKeyNames.stream().map(i -> + PostImage.builder() + .imageKeyName(i == null || i.isEmpty() || i.isBlank() ? null : s3Service.generateStaticUrl(i)) + .post(updatePost) + .build() + ).toList(); + + updatePost.updatePost(updateMoimPostDTO.title(), updateMoimPostDTO.content(), imageList); + + return updatePost; + } + + @Override + public void blockPost(User user, PostBlockDTO postBlockDTO) { + Post post = postRepository.findById(postBlockDTO.postId()).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + + Optional postBlock = postBlockRepository.findByUserIdAndPostId(user.getId(), postBlockDTO.postId()); + + if (postBlock.isPresent()) { + postBlockRepository.delete(postBlock.get()); + } else { + PostBlock savedPostBlock = PostBlock.builder() + .user(user) + .post(post) + .build(); + + postBlockRepository.save(savedPostBlock); + } + } + + @Override + public void deleteComment(User user, Long commentId) { + Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new CommentException(ErrorStatus.COMMENT_NOT_FOUND)); + if (!user.equals(comment.getUserMoim().getUser())){ + throw new PostException(ErrorStatus.NOT_MY_POST); + } + + + if (comment.getParent() != null) { + // 대댓 일시 + commentRepository.delete(comment); + } else { + // 댓글 일시 + if (comment.getChildren().isEmpty()) { + // 자식이 없는 댓글일 시 + commentRepository.delete(comment); + } else { + comment.delete(); + } + } + } + + @Override + public void updateComment(User user, CommentUpdateRequestDTO commentUpdateRequestDTO) { + Comment comment = commentRepository.findById(commentUpdateRequestDTO.commentId()).orElseThrow(() -> new CommentException(ErrorStatus.COMMENT_NOT_FOUND)); + + User user2 = null; + try { + user2 = comment.getUserMoim().getUser(); + } catch (Exception e) { + throw new PostException(ErrorStatus.ALREADY_COMMENT_DELETE); + } + + if (!user.equals(user2)){ + throw new PostException(ErrorStatus.NOT_MY_POST); + } + + comment.update(commentUpdateRequestDTO.content()); + } + + @Override + public void reportComment(User user, CommentReportDTO commentReportDTO) { + Comment comment = commentRepository.findById(commentReportDTO.commentId()).orElseThrow(() -> new CommentException(ErrorStatus.COMMENT_NOT_FOUND)); + + if (!comment.getPost().getId().equals(commentReportDTO.postId())) { + throw new PostException(ErrorStatus.NOT_INCLUDE_POST); + } + + Optional commentReport = commentReportRepository.findByUserAndComment(user, comment); + + if (commentReport.isPresent()) { + // 이미 있음. + commentReportRepository.delete(commentReport.get()); + } else { + + // 없음 -> 삭제 + CommentReport savedPostReport = CommentReport.builder() + .comment(comment) + .user(user) + .build(); + + commentReportRepository.save(savedPostReport); + } + } + + @Override + public void blockComment(User user, CommentBlockDTO commentBlockDTO) { + Comment comment = commentRepository.findById(commentBlockDTO.commentId()).orElseThrow(() -> new CommentException(ErrorStatus.COMMENT_NOT_FOUND)); + + if (!comment.getPost().getId().equals(commentBlockDTO.postId())) { + throw new PostException(ErrorStatus.NOT_INCLUDE_POST); + } + + Optional commentBlock = commentBlockRepository.findByUserAndComment(user, comment); + + if (commentBlock.isPresent()) { + // 이미 있음. + commentBlockRepository.delete(commentBlock.get()); + } else { + + // 없음 -> 삭제 + CommentBlock savedCommentBlock = CommentBlock.builder() + .comment(comment) + .user(user) + .build(); + + commentBlockRepository.save(savedCommentBlock); + } + } + + @Override + public Long createAnnouncement(User user, AnnouncementRequestDTO announcementRequestDTO) { + Moim moim = moimRepository.findById(announcementRequestDTO.moimId()).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moim.getId(), JoinStatus.COMPLETE).orElseThrow(()-> new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN)); + + Post savedPost = Post.builder() + .title(announcementRequestDTO.title()) + .content(announcementRequestDTO.content()) + .postType(PostType.ANNOUNCEMENT) + .userMoim(userMoim) + .moim(moim) + .build(); + + postRepository.save(savedPost); + + announcementRequestDTO.imageKeyNames().forEach((i) ->{ + PostImage postImage = PostImage.builder().imageKeyName(i == null || i.isEmpty() || i.isBlank() ? null : s3Service.generateStaticUrl(i)).post(savedPost).build(); + postImageRepository.save(postImage); + } + ); + + List unReadUserIds; + if (announcementRequestDTO.isAllUserSelected()) { + unReadUserIds = userRepository.findUserByMoim(moim, JoinStatus.COMPLETE).stream().map(User::getId).toList(); + } else { + unReadUserIds = announcementRequestDTO.userIds(); + } + + + List allById = userRepository.findAllById(unReadUserIds); + List readPosts = allById.stream().map((u) -> + ReadPost.builder().post(savedPost).user(u).isRead(false).build() + ).toList(); + + readPostRepository.saveAll(readPosts); + + + List userByMoim = userRepository.findUserByMoim(moim, JoinStatus.COMPLETE); + if (savedPost.getPostType().equals(PostType.ANNOUNCEMENT)) { + String name = moim.getName(); + String moimName = (name != null && name.length() >= 7) + ? name.substring(0, 7) + : (name != null ? name : ""); + userByMoim.forEach((u) ->{ + if (u.getIsPushAlarm() && u != user) { + alarmService.saveAlarm(user, u, "[" + moimName +"] 새로운 공지사항이 있습니다.", savedPost.getTitle(), AlarmType.PUSH, AlarmDetailType.POST, moim.getId(), savedPost.getId(), null); + fcmService.sendPushNotification(u, "[" + moimName +"] 새로운 공지사항이 있습니다.", savedPost.getTitle(), AlarmDetailType.POST); + } + }); + } + + return savedPost.getId(); + } + + @Override + public void announcementConfirm(User user, AnnouncementConfirmRequestDTO announcementRequestDTO) { + Post post = postRepository.findById(announcementRequestDTO.postId()).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + + ReadPost readPost = readPostRepository.findByUserAndPost(user, post).orElseThrow(() -> new PostException(ErrorStatus._FORBIDDEN)); + readPost.read(); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/PostQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/PostQueryServiceImpl.java new file mode 100644 index 00000000..508960af --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/PostQueryServiceImpl.java @@ -0,0 +1,192 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; + +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.moim.controller.enums.PostRequestType; +import com.dev.moim.domain.moim.converter.PostConverter; +import com.dev.moim.domain.moim.dto.post.*; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.enums.PostType; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.PostQueryService; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.PostException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostQueryServiceImpl implements PostQueryService { + + private final PostRepository postRepository; + private final MoimRepository moimRepository; + private final UserMoimRepository userMoimRepository; + private final CommentRepository commentRepository; + private final CommentLikeRepository commentLikeRepository; + private final PostLikeRepository postLikeRepository; + private final PostBlockRepository postBlockRepository; + + @Override + public MoimPostPreviewListDTO getMoimPostList(User user, Long moimId, PostRequestType postRequestType, Long cursor, Integer take) { + Moim moim = moimRepository.findById(moimId).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + Slice postSlices; + if (postRequestType.equals(PostRequestType.ALL)) { + postSlices = postRepository.findByMoimAndIdLessThanAndUserPostBlocksNotInOrderByIdDesc(moim, cursor, user, PageRequest.of(0, take)); + } else { + PostType postType = PostType.valueOf(postRequestType.toString()); + postSlices = postRepository.findByMoimAndPostTypeAndIdLessThanAndUserPostBlocksNotInOrderByIdDesc(moim, postType, cursor, user, PageRequest.of(0, take)); + } + + List moimPostPreviewDTOList = postSlices.stream().map((p)->{ + Optional userMoim = userMoimRepository.findByPost(p); + return MoimPostPreviewDTO.toMoimPostPreviewDTO(p, userMoim); + }).toList(); + + Long nextCursor = null; + if (!postSlices.isLast()) { + nextCursor = postSlices.toList().get(postSlices.toList().size() - 1).getId(); + } + + return PostConverter.toMoimPostPreviewListDTO(moimPostPreviewDTOList, postSlices.hasNext(), nextCursor); + } + + @Override + public MoimPostDetailDTO getMoimPost(User user, Long moimId, Long postId) { + Moim moim = moimRepository.findById(moimId).orElseThrow(()-> new MoimException(ErrorStatus.MOIM_NOT_FOUND)); + + Optional byUserIdAndPostId = postBlockRepository.findByUserIdAndPostId(user.getId(), postId); + + if (byUserIdAndPostId.isPresent()) { + throw new PostException(ErrorStatus.BLOCK_POST); + } + + if(!userMoimRepository.existsByUserAndMoim(user, moim)) { + throw new MoimException(ErrorStatus.USER_NOT_MOIM_JOIN); + } + + Post post = postRepository.findById(postId).orElseThrow(() -> new PostException(ErrorStatus.POST_NOT_FOUND)); + + Boolean postLike = isPostLike(user.getId(), postId); + + Optional userMoim = userMoimRepository.findByPost(post); + + return MoimPostDetailDTO.toMoimPostDetailDTO(post, postLike, userMoim); + } + + @Override + public Boolean isCommentLike(Long userId, Long commentId) { + return commentLikeRepository.existsCommentLikeByUserIdAndCommentId(userId, commentId); + } + + @Override + public CommentResponseListDTO getcomments(User user, Long moimId, Long postId, Long cursor, Integer take) { + + Post post = postRepository.findById(postId).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + + + Slice commentSlices = commentRepository.findByPostAndIdGreaterThanAndParentIsNullOrderByIdAsc(post, cursor, PageRequest.of(0, take)); + + List commentBlockList = commentRepository.findByUserAndPostId(user, postId); + + Long nextCursor = null; + if (!commentSlices.isLast()) { + nextCursor = commentSlices.toList().get(commentSlices.toList().size() - 1).getId(); + } + + List commentResponseDTOList = commentSlices.stream().map((comment) -> { + Optional commentUserMoim = userMoimRepository.findByComment(comment); + List commentCommentResponseDTOList = comment.getChildren().stream().map(commentcomment -> { + Optional commentCommentUserMoim = userMoimRepository.findByComment(commentcomment); + return CommentCommentResponseDTO.toCommentCommentResponseDTO(commentcomment, isCommentLike(user.getId(), commentcomment.getId()), commentBlockList, commentCommentUserMoim); + }).toList(); + return CommentResponseDTO.toCommentResponseDTO(comment, isCommentLike(user.getId(), comment.getId()), commentCommentResponseDTOList, commentBlockList, commentUserMoim); + }).toList(); + + return CommentResponseListDTO.toCommentResponseListDTO(commentResponseDTOList, nextCursor, commentSlices.hasNext()); + } + + @Override + public Boolean isPostLike(Long userId, Long postId) { + return postLikeRepository.existsPostLikeByUserIdAndPostId(userId, postId); + } + + @Override + public MoimPostPreviewListDTO getIntroductionPosts(Long cursor, Integer take) { + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + Slice postSlices = postRepository.findByPostTypeAndIdLessThanOrderByIdDesc(PostType.GLOBAL, cursor, PageRequest.of(0, take)); + + List moimPostPreviewDTOList = postSlices.stream().map((p)->{ + Optional userMoim = userMoimRepository.findByPost(p); + return MoimPostPreviewDTO.toMoimPostPreviewDTO(p, userMoim); + }).toList(); + + Long nextCursor = null; + if (!postSlices.isLast()) { + nextCursor = postSlices.toList().get(postSlices.toList().size() - 1).getId(); + } + + return PostConverter.toMoimPostPreviewListDTO(moimPostPreviewDTOList, postSlices.hasNext(), nextCursor); + } + + @Override + public Post getIntroductionPost(Long postId) { + + return postRepository.findById(postId).orElseThrow(()-> new PostException(ErrorStatus.POST_NOT_FOUND)); + } + + @Override + public List getPostsByJoinMoims(User user) { + List moimsByUser = moimRepository.findMoimsByUser(user); + + List joinMoimPostsResponseDTOList = moimsByUser.stream().map((m) -> { + List postList = postRepository.findByNotPostTypeAndMoimOrderByCreatedAtDesc(PostType.GLOBAL ,m, PageRequest.of(0, 3)); + List moimPostPreviewDTOStream = postList.stream().map((p)->{ + Optional userMoim = userMoimRepository.findByPost(p); + return MoimPostPreviewDTO.toMoimPostPreviewDTO(p, userMoim); + }).toList(); + + return JoinMoimPostsResponseDTO.toJoinMoimPostsResponseDTO(m.getId(), m.getName(), moimPostPreviewDTOStream); + }).toList(); + + return joinMoimPostsResponseDTOList; + } + + @Override + public List findBlockComments(User user) { + + List comments = commentRepository.findByBlockComment(user); + + return comments.stream().map(BlockCommentResponse::toBlockCommentResponse).toList(); + } + + @Override + public List findBlockPosts(User user) { + + List postList = postRepository.findBlockPost(user); + + return postList.stream().map((p)->{ + Optional userMoim = userMoimRepository.findByPost(p); + return MoimPostPreviewDTO.toMoimPostPreviewDTO(p, userMoim); + }).toList(); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/TodoCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/TodoCommandServiceImpl.java new file mode 100644 index 00000000..262a3a5d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/TodoCommandServiceImpl.java @@ -0,0 +1,225 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.moim.dto.todo.*; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.domain.moim.entity.enums.TodoStatus; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.TodoCommandService; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.TodoException; +import com.dev.moim.global.firebase.service.FcmService; +import com.dev.moim.global.s3.service.S3Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional +public class TodoCommandServiceImpl implements TodoCommandService { + + private final MoimRepository moimRepository; + private final TodoRepository todoRepository; + private final TodoImageRepository todoImageRepository; + private final S3Service s3Service; + private final UserRepository userRepository; + private final UserTodoRepository userTodoRepository; + private final UserMoimRepository userMoimRepository; + private final AlarmService alarmService; + private final FcmService fcmService; + + @Override + public Long createTodo(User user, Long moimId, CreateTodoDTO request) { + + Moim moim = moimRepository.findById(moimId) + .orElseThrow(() -> new MoimException(MOIM_NOT_FOUND)); + + Todo todo = Todo.builder() + .title(request.title()) + .content(request.content()) + .dueDate(request.dueDate().atTime(23, 59, 59, 999999000)) + .status(TodoStatus.IN_PROGRESS) + .moim(moim) + .writer(user) + .build(); + + todoRepository.save(todo); + + request.imageKeyList().forEach((i) -> + todoImageRepository.save(TodoImage.builder() + .imageUrl((i == null || i.isEmpty() || i.isBlank()) ? null : s3Service.generateStaticUrl(i)) + .todo(todo) + .build())); + + List userList = request.isAssigneeSelectAll() + ? userMoimRepository.findByMoimIdAndJoinStatus(moimId, JoinStatus.COMPLETE).stream() + .map(UserMoim::getUser).toList() + : userRepository.findAllById(request.targetUserIdList()); + + List userTodoList = userList.stream() + .map(userEntity -> UserTodo.builder() + .user(userEntity) + .todo(todo) + .status(TodoAssigneeStatus.PENDING) + .build()) + .toList(); + + userTodoRepository.saveAll(userTodoList); + + userList.stream().filter(assignee -> !assignee.equals(user)) + .forEach(assignee -> { + alarmService.saveAlarm(user, assignee, "새로운 할 일이 도착했습니다", todo.getTitle(), AlarmType.PUSH, AlarmDetailType.TODO, moim.getId(), null, null); + + if (assignee.getIsPushAlarm() && assignee.getDeviceId() != null) { + fcmService.sendPushNotification(assignee, "새로운 할 일이 도착했습니다", todo.getTitle(), AlarmDetailType.TODO); + } + }); + + return todo.getId(); + } + + @Override + public UpdateTodoStatusResponseDTO updateUserTodoStatus(User user, Long todoId, UpdateTodoStatusDTO request) { + + UserTodo userTodo = userTodoRepository.findByUserIdAndTodoId(user.getId(), todoId) + .orElseThrow(() -> new TodoException(TODO_NOT_FOUND)); + + userTodo.updateStatus(request.todoAssigneeStatus()); + + Todo todo = userTodo.getTodo(); + + todo.updateStatus( + userTodoRepository.findAllByTodoId(todoId).stream() + .map(UserTodo::getStatus) + .allMatch(status -> status == TodoAssigneeStatus.COMPLETE) + ? TodoStatus.COMPLETED : TodoStatus.IN_PROGRESS + ); + + return UpdateTodoStatusResponseDTO.of(userTodo); + } + + @Override + public void updateTodo(User user, Long moimId, Long todoId, UpdateTodoDTO request) { + + Todo todo = todoRepository.findById(todoId) + .orElseThrow(() -> new TodoException(TODO_NOT_FOUND)); + + LocalDateTime dueDateTime = request.dueDate().atTime(23, 59, 59, 999999000); + + for (UserTodo userTodo : todo.getUserTodoList()) { + if (!dueDateTime.equals(todo.getDueDate())) { + if (dueDateTime.isAfter(LocalDateTime.now())) { + userTodo.updateStatus(switch (userTodo.getStatus()) { + case LOADING, COMPLETE -> TodoAssigneeStatus.LOADING; + case OVERDUE -> TodoAssigneeStatus.PENDING; + default -> userTodo.getStatus(); + }); + } + } else { + userTodo.updateStatus(switch (userTodo.getStatus()) { + case LOADING, COMPLETE -> TodoAssigneeStatus.LOADING; + case OVERDUE -> TodoAssigneeStatus.PENDING; + default -> userTodo.getStatus(); + }); + } + } + + List newImageList = (request.imageKeyList() != null) ? + request.imageKeyList().stream() + .map(imageKey -> TodoImage.builder() + .imageUrl(s3Service.generateStaticUrl(imageKey)) + .todo(todo) + .build()) + .toList() + : null; + + todo.updateTodo( + request.title(), + request.content(), + dueDateTime, + newImageList + ); + + todo.getUserTodoList().stream().map(UserTodo::getUser).filter(assignee -> !assignee.equals(user)) + .forEach(assignee -> { + alarmService.saveAlarm(user, assignee, "할 일이 수정되었습니다", todo.getTitle(), AlarmType.PUSH, AlarmDetailType.TODO, moimId, null, null); + + if (assignee.getIsPushAlarm() && assignee.getDeviceId() != null) { + fcmService.sendPushNotification(assignee, "할 일이 수정되었습니다", todo.getTitle(), AlarmDetailType.TODO); + } + }); + } + + @Override + public void deleteTodo(Long todoId) { + todoRepository.deleteById(todoId); + } + + @Override + public void addAssignees(User user, AddTodoAssigneeDTO request) { + + Todo todo = todoRepository.findById(request.todoId()) + .orElseThrow(() -> new TodoException(TODO_NOT_FOUND)); + + List userList = userRepository.findAllById(request.addAssigneeIdList()); + + List userTodoListToAdd = userList.stream() + .map(userEntity -> UserTodo.builder() + .user(userEntity) + .todo(todo) + .status(TodoAssigneeStatus.PENDING) + .build()) + .toList(); + + todo.getUserTodoList().addAll(userTodoListToAdd); + + userTodoListToAdd.stream().map(UserTodo::getUser).filter(assignee -> !assignee.equals(user)) + .forEach(assignee -> { + alarmService.saveAlarm(user, assignee, "새로운 할 일이 도착했습니다", todo.getTitle(), AlarmType.PUSH, AlarmDetailType.TODO, request.moimId(), null, null); + + if (assignee.getIsPushAlarm() && assignee.getDeviceId() != null) { + fcmService.sendPushNotification(assignee, "새로운 할 일이 도착했습니다", todo.getTitle(), AlarmDetailType.TODO); + } + }); + } + + @Override + public void deleteAssignees(DeleteTodoAssigneeDTO request) { + + Todo todo = todoRepository.findById(request.todoId()) + .orElseThrow(() -> new TodoException(TODO_NOT_FOUND)); + + List userTodoList = request.deleteAssigneeIdList().stream() + .map(id -> userTodoRepository.findByUserIdAndTodoId(id, request.todoId()) + .orElseThrow(() -> new TodoException(NOT_TODO_ASSIGNEE))).toList(); + + todo.getUserTodoList().removeAll(userTodoList); + } + + @Override + public void updateExpiredTodosAndAssigneesStatus() { + + List expiredTodoList = todoRepository.findAllByStatusAndDueDateBefore(TodoStatus.IN_PROGRESS, LocalDateTime.now()); + + expiredTodoList.forEach(todo -> { + List incompleteUserTodoList = userTodoRepository.findAllByTodoIdAndStatusNot(todo.getId(), TodoAssigneeStatus.COMPLETE); + incompleteUserTodoList.forEach(userTodo -> userTodo.updateStatus(TodoAssigneeStatus.OVERDUE)); + + todo.updateStatus(TodoStatus.EXPIRED); + }); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/TodoQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/moim/service/impl/TodoQueryServiceImpl.java new file mode 100644 index 00000000..0d5dbe76 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/TodoQueryServiceImpl.java @@ -0,0 +1,233 @@ +package com.dev.moim.domain.moim.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.todo.TodoAssigneeDetailDTO; +import com.dev.moim.domain.moim.dto.todo.TodoDTO; +import com.dev.moim.domain.moim.dto.todo.TodoDetailDTO; +import com.dev.moim.domain.moim.dto.todo.TodoPageDTO; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.repository.TodoRepository; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import com.dev.moim.domain.moim.repository.UserTodoRepository; +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.TodoException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class TodoQueryServiceImpl implements TodoQueryService { + + private final UserTodoRepository userTodoRepository; + private final TodoRepository todoRepository; + private final UserMoimRepository userMoimRepository; + + @Override + public TodoDetailDTO getTotalDetailForAssignee(User user, Long todoId) { + + Todo todo = todoRepository.findById(todoId) + .orElseThrow(() -> new TodoException(TODO_NOT_FOUND)); + + UserTodo userTodo = userTodoRepository.findByUserIdAndTodoId(user.getId(), todoId) + .orElseThrow(() -> new TodoException(NOT_TODO_ASSIGNEE)); + + return new TodoDetailDTO( + todo.getId(), + todo.getMoim().getId(), + todo.getTitle(), + todo.getContent(), + todo.getDueDate(), + todo.getTodoImageList().stream().map(TodoImage::getImageUrl).toList(), + userTodo.getStatus(), + todo.getStatus() + ); + } + + @Override + public TodoDetailDTO getTodoDetailForAdmin(Long todoId) { + + Todo todo = todoRepository.findById(todoId) + .orElseThrow(() -> new TodoException(TODO_NOT_FOUND)); + + return new TodoDetailDTO( + todo.getId(), + todo.getMoim().getId(), + todo.getTitle(), + todo.getContent(), + todo.getDueDate(), + todo.getTodoImageList().stream().map(TodoImage::getImageUrl).toList(), + null, + todo.getStatus() + ); + } + + @Override + public TodoPageDTO getTodoAssigneeListForAdmin(Long todoId, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? 0L : cursor; + Pageable pageable = PageRequest.of(0, take); + + Slice userTodoSlice = userTodoRepository.findAllWithUserMoimAndUserProfileByTodoIdAndCursor(todoId, startCursor, pageable); + + List todoAssigneeDetailDTOList = userTodoSlice.stream() + .map(userTodo -> { + User user = userTodo.getUser(); + Moim moim = userTodo.getTodo().getMoim(); + + UserMoim userMoim = user.getUserMoimList().stream() + .filter(um -> um.getMoim().equals(moim)) + .findFirst() + .orElseThrow(() -> new MoimException(USER_MOIM_NOT_FOUND)); + + return TodoAssigneeDetailDTO.toTodoAssignee(userTodo, userMoim); + }) + .toList(); + + Long nextCursor = userTodoSlice.hasNext() && !userTodoSlice.getContent().isEmpty() + ? userTodoSlice.getContent().get(userTodoSlice.getNumberOfElements() - 1).getId() + : null; + + return new TodoPageDTO(todoAssigneeDetailDTOList, nextCursor, userTodoSlice.hasNext()); + } + + @Override + public TodoPageDTO getTodoNonAssigneeListForAdmin(Long moimId, Long todoId, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? 0L : cursor; + Pageable pageable = PageRequest.of(0, take); + + Slice userMoimSlice = userMoimRepository.findAllMembersNotAssignedToTodo(moimId, todoId, JoinStatus.COMPLETE, startCursor, pageable); + + List todoAssigneeDetailDTOList = userMoimSlice.stream() + .map(TodoAssigneeDetailDTO::toTodoNonAssignee) + .toList(); + + Long nextCursor = userMoimSlice.hasNext() && !userMoimSlice.getContent().isEmpty() + ? userMoimSlice.getContent().get(userMoimSlice.getNumberOfElements() - 1).getId() + : null; + + return new TodoPageDTO(todoAssigneeDetailDTOList, nextCursor, userMoimSlice.hasNext()); + } + + @Override + public TodoPageDTO getMoimTodoListForAdmin(Long moimId, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? Long.MAX_VALUE : cursor; + Pageable pageable = PageRequest.of(0, take, Sort.by(Sort.Order.desc("id"))); + + Slice todoSlice = todoRepository.findByMoimIdAndCursorLessThan(moimId, startCursor, pageable); + + Set writerIds = todoSlice.getContent().stream() + .map(todo -> todo.getWriter().getId()) + .collect(Collectors.toSet()); + List userMoimList = userMoimRepository.findByMoimIdAndUserIds(moimId, writerIds); + Map userMoimMap = userMoimList.stream() + .collect(Collectors.toMap(um -> um.getUser().getId(), um -> um)); + + List todoDTOList = todoSlice.getContent().stream() + .map(todo -> { + Long writerId = todo.getWriter().getId(); + Optional userMoim = Optional.ofNullable(userMoimMap.get(writerId)); + return TodoDTO.forMoimAdmins(todo, userMoim); + }) + .toList(); + + Long nextCursor = todoSlice.hasNext() ? todoSlice.getContent().get(todoSlice.getNumberOfElements() - 1).getId() : null; + + return new TodoPageDTO(todoDTOList, nextCursor, todoSlice.hasNext()); + } + + @Override + public TodoPageDTO getSpecificMoimTodoListByMe(User user, Long moimId, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? Long.MAX_VALUE : cursor; + Pageable pageable = PageRequest.of(0, take, Sort.by(Sort.Order.desc("id"))); + + Slice todoSlice = todoRepository.findByWriterIdAndMoimIdAndCursorLessThan(user.getId(), moimId, startCursor, pageable); + + List todoDTOList = todoSlice.getContent().stream() + .map(TodoDTO::forSpecificAdmin) + .toList(); + + Long nextCursor = todoSlice.hasNext() ? todoSlice.getContent().get(todoSlice.getNumberOfElements() - 1).getId() : null; + + return new TodoPageDTO(todoDTOList, nextCursor, todoSlice.hasNext()); + } + + @Override + public TodoPageDTO getAssignedTodoListForUserInSpecificMoim(User user, Long moimId, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? Long.MAX_VALUE : cursor; + Pageable pageable = PageRequest.of(0, take, Sort.by(Sort.Order.desc("id"))); + + Slice userTodoSlice = userTodoRepository.findUserTodosByUserIdAndMoimId(user.getId(), moimId, startCursor, pageable); + + List todoDTOList = userTodoSlice.getContent().stream() + .map(userTodo -> TodoDTO.forAssignee(userTodo.getTodo(), userTodo)) + .toList(); + + Long nextCursor = userTodoSlice.hasNext() ? userTodoSlice.getContent().get(userTodoSlice.getNumberOfElements() - 1).getId() : null; + + return new TodoPageDTO(todoDTOList, nextCursor, userTodoSlice.hasNext()); + } + + @Override + public TodoPageDTO getTodoListByMe(User user, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? Long.MAX_VALUE : cursor; + Pageable pageable = PageRequest.of(0, take, Sort.by(Sort.Order.desc("id"))); + + Slice todoSlice = todoRepository.findByWriterIdAndCursorLessThan(user.getId(), startCursor, pageable); + + List todoDTOList = todoSlice.getContent().stream() + .map(TodoDTO::forSpecificAdmin) + .toList(); + + Long nextCursor = todoSlice.hasNext() ? todoSlice.getContent().get(todoSlice.getNumberOfElements() - 1).getId() : null; + + return new TodoPageDTO(todoDTOList, nextCursor, todoSlice.hasNext()); + } + + @Override + public boolean existsByUserIdAndTodoId(Long userId, Long todoId) { + return userTodoRepository.existsByUserIdAndTodoId(userId, todoId); + } + + @Override + public boolean existsByTodoId(Long todoId) { + return todoRepository.existsById(todoId); + } + + @Override + public Optional findByUserIdAndTodoId(Long userId, Long todoId) { + return userTodoRepository.findByUserIdAndTodoId(userId, todoId); + } + + @Override + public Optional findTodoByTodoId(Long todoId) { + return todoRepository.findById(todoId); + } + + @Override + public List findAssigneeByTodoId(Long todoId) { + return userTodoRepository.findAllByTodoId(todoId); + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/dto/IntroduceVideoDTO.java b/src/main/java/com/dev/moim/domain/moim/service/impl/dto/IntroduceVideoDTO.java new file mode 100644 index 00000000..710ad6b8 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/dto/IntroduceVideoDTO.java @@ -0,0 +1,16 @@ +package com.dev.moim.domain.moim.service.impl.dto; + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.Moim; +import lombok.Getter; + +@Getter +public class IntroduceVideoDTO { + private Moim moim; + private UserProfile userProfile; + + public IntroduceVideoDTO(Moim moim, UserProfile userProfile) { + this.moim = moim; + this.userProfile = userProfile; + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/dto/JoinRequestDTO.java b/src/main/java/com/dev/moim/domain/moim/service/impl/dto/JoinRequestDTO.java new file mode 100644 index 00000000..dc709782 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/dto/JoinRequestDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.moim.service.impl.dto; + +import com.dev.moim.domain.moim.entity.Moim; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import lombok.Getter; + +@Getter +public class JoinRequestDTO { + private Moim moim; + private UserMoim userMoim; + + public JoinRequestDTO(Moim moim, UserMoim userMoim) { + this.moim = moim; + this.userMoim = userMoim; + } +} diff --git a/src/main/java/com/dev/moim/domain/moim/service/impl/dto/UserProfileDTO.java b/src/main/java/com/dev/moim/domain/moim/service/impl/dto/UserProfileDTO.java new file mode 100644 index 00000000..15ca1f1a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/moim/service/impl/dto/UserProfileDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.moim.service.impl.dto; + + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.UserMoim; +import lombok.Getter; + +@Getter +public class UserProfileDTO { + private UserProfile userProfile; + private UserMoim userMoim; + + public UserProfileDTO(UserProfile userProfile, UserMoim userMoim) { + this.userProfile = userProfile; + this.userMoim = userMoim; + } +} diff --git a/src/main/java/com/dev/moim/domain/region/controller/RegionController.java b/src/main/java/com/dev/moim/domain/region/controller/RegionController.java new file mode 100644 index 00000000..e5c3415d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/controller/RegionController.java @@ -0,0 +1,72 @@ +package com.dev.moim.domain.region.controller; + +import com.dev.moim.domain.region.dto.RegionListDTO; +import com.dev.moim.domain.region.dto.RegionSearchListDTO; +import com.dev.moim.domain.region.service.RegionQueryService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.validation.annotation.CheckTakeValidation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/regions") +@Tag(name = "주소 관련 컨트롤러") +public class RegionController { + + private final RegionQueryService regionQueryService; + + @GetMapping("/sido") + @Operation(summary="행정구역 (시/도) 조회 API", description="행정구역 (시/도) 조회 API 입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + public BaseResponse getSido( + ) { + return BaseResponse.onSuccess(regionQueryService.getSido()); + } + + @GetMapping("/sigungu") + @Operation(summary="행정구역 (시/군/구) 조회 API", description="행정구역 (시/군/구) 조회 API 입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + public BaseResponse getSigungu( + @Parameter(description = "상위 시/도 ID") @RequestParam Long parentId + ) { + return BaseResponse.onSuccess(regionQueryService.getSigungu(parentId)); + } + + @GetMapping("/dong") + @Operation(summary="행정구역 (동) 조회 API", description="행정구역 (동) 조회 API 입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + public BaseResponse getDong( + @Parameter(description = "상위 시/군/구 ID") @RequestParam Long parentId + ) { + return BaseResponse.onSuccess(regionQueryService.getDong(parentId)); + } + + @GetMapping("/search") + @Operation(summary="행정구역 검색 API", description="행정구역 검색 API 입니다.") + @Parameter(name = "searchTerm", description = "해당 검색어가 포함된 행정구역을 조회합니다.", example = "서울특별시") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + public BaseResponse getRegions( + @RequestParam(name = "searchTerm") String searchTerm, + @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(regionQueryService.getRegions(searchTerm, cursor, take)); + } +} diff --git a/src/main/java/com/dev/moim/domain/region/dto/RegionDTO.java b/src/main/java/com/dev/moim/domain/region/dto/RegionDTO.java new file mode 100644 index 00000000..b8e96ad2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/dto/RegionDTO.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.region.dto; + +public record RegionDTO( + Long regionId, + String regionName +) { +} diff --git a/src/main/java/com/dev/moim/domain/region/dto/RegionListDTO.java b/src/main/java/com/dev/moim/domain/region/dto/RegionListDTO.java new file mode 100644 index 00000000..9668c5ce --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/dto/RegionListDTO.java @@ -0,0 +1,35 @@ +package com.dev.moim.domain.region.dto; + +import com.dev.moim.domain.region.entity.Dong; +import com.dev.moim.domain.region.entity.Sido; +import com.dev.moim.domain.region.entity.Sigungu; + +import java.util.List; + +public record RegionListDTO( + List regionList +) { + public static RegionListDTO toSido(List sidoList) { + List regionList = sidoList.stream() + .map(sido -> new RegionDTO(sido.getId(), sido.getAddrName())) + .toList(); + + return new RegionListDTO(regionList); + } + + public static RegionListDTO toSigungu(List siguguList) { + List regionList = siguguList.stream() + .map(sigugu -> new RegionDTO(sigugu.getId(), sigugu.getAddrName())) + .toList(); + + return new RegionListDTO(regionList); + } + + public static RegionListDTO toDong(List dongList) { + List regionList = dongList.stream() + .map(dong -> new RegionDTO(dong.getId(), dong.getAddrName())) + .toList(); + + return new RegionListDTO(regionList); + } +} diff --git a/src/main/java/com/dev/moim/domain/region/dto/RegionSearchDTO.java b/src/main/java/com/dev/moim/domain/region/dto/RegionSearchDTO.java new file mode 100644 index 00000000..14f42e72 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/dto/RegionSearchDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.region.dto; + +import com.dev.moim.domain.region.entity.Dong; + +public record RegionSearchDTO( + Long dongId, + String regionTotalName +) { + public static RegionSearchDTO of(Dong dong) { + String sidoName = dong.getParent().getParent().getAddrName(); + String sigunguName = dong.getParent().getAddrName(); + return new RegionSearchDTO( + dong.getId(), + sidoName + " " + sigunguName + " " + dong.getAddrName() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/region/dto/RegionSearchListDTO.java b/src/main/java/com/dev/moim/domain/region/dto/RegionSearchListDTO.java new file mode 100644 index 00000000..09cd2ae1 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/dto/RegionSearchListDTO.java @@ -0,0 +1,28 @@ +package com.dev.moim.domain.region.dto; + +import com.dev.moim.domain.region.entity.Dong; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record RegionSearchListDTO( + List regionSearchDTOList, + Long nextCursor, + Boolean hasNext +) { + public static RegionSearchListDTO of(Slice dongSlice) { + List regionSearchDTOList = dongSlice.stream() + .map(RegionSearchDTO::of) + .toList(); + + Long nextCursor = dongSlice.hasNext() + ? dongSlice.getContent().get(dongSlice.getNumberOfElements() - 1).getId() + 1 + : null; + + return new RegionSearchListDTO( + regionSearchDTOList, + nextCursor, + dongSlice.hasNext() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/region/entity/Dong.java b/src/main/java/com/dev/moim/domain/region/entity/Dong.java new file mode 100644 index 00000000..37fc3982 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/entity/Dong.java @@ -0,0 +1,22 @@ +package com.dev.moim.domain.region.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Dong { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String addrName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Sigungu parent; +} diff --git a/src/main/java/com/dev/moim/domain/region/entity/Sido.java b/src/main/java/com/dev/moim/domain/region/entity/Sido.java new file mode 100644 index 00000000..3a5eb830 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/entity/Sido.java @@ -0,0 +1,21 @@ +package com.dev.moim.domain.region.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Sido { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String addrName; +} diff --git a/src/main/java/com/dev/moim/domain/region/entity/Sigungu.java b/src/main/java/com/dev/moim/domain/region/entity/Sigungu.java new file mode 100644 index 00000000..d0913c2b --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/entity/Sigungu.java @@ -0,0 +1,22 @@ +package com.dev.moim.domain.region.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Sigungu { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String addrName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Sido parent; +} diff --git a/src/main/java/com/dev/moim/domain/region/repository/DongRepository.java b/src/main/java/com/dev/moim/domain/region/repository/DongRepository.java new file mode 100644 index 00000000..c7cc3f15 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/repository/DongRepository.java @@ -0,0 +1,24 @@ +package com.dev.moim.domain.region.repository; + +import com.dev.moim.domain.region.entity.Dong; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface DongRepository extends JpaRepository { + List findByParentId(Long parentId); + + @Query("SELECT d FROM Dong d " + + "JOIN d.parent sg " + + "JOIN sg.parent s " + + "WHERE (s.addrName LIKE %:searchTerm% " + + "OR sg.addrName LIKE %:searchTerm% " + + "OR d.addrName LIKE %:searchTerm%) " + + "AND (d.id >= :cursor) " + + "ORDER BY d.id ASC") + Slice searchAddress(@Param("searchTerm") String searchTerm, @Param("cursor") Long cursor, Pageable pageable); +} diff --git a/src/main/java/com/dev/moim/domain/region/repository/SidoRepository.java b/src/main/java/com/dev/moim/domain/region/repository/SidoRepository.java new file mode 100644 index 00000000..3ff7b4f8 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/repository/SidoRepository.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.region.repository; + +import com.dev.moim.domain.region.entity.Sido; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SidoRepository extends JpaRepository { +} diff --git a/src/main/java/com/dev/moim/domain/region/repository/SigunguRepository.java b/src/main/java/com/dev/moim/domain/region/repository/SigunguRepository.java new file mode 100644 index 00000000..d43c5ac1 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/repository/SigunguRepository.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.region.repository; + +import com.dev.moim.domain.region.entity.Sigungu; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SigunguRepository extends JpaRepository { + List findByParentId(Long parentId); +} diff --git a/src/main/java/com/dev/moim/domain/region/service/RegionQueryService.java b/src/main/java/com/dev/moim/domain/region/service/RegionQueryService.java new file mode 100644 index 00000000..7220f9eb --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/service/RegionQueryService.java @@ -0,0 +1,15 @@ +package com.dev.moim.domain.region.service; + +import com.dev.moim.domain.region.dto.RegionListDTO; +import com.dev.moim.domain.region.dto.RegionSearchListDTO; + +public interface RegionQueryService { + + RegionListDTO getSido(); + + RegionListDTO getSigungu(Long parentId); + + RegionListDTO getDong(Long parentId); + + RegionSearchListDTO getRegions(String searchTerm, Long cursor, Integer take); +} diff --git a/src/main/java/com/dev/moim/domain/region/service/impl/RegionQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/region/service/impl/RegionQueryServiceImpl.java new file mode 100644 index 00000000..d90cb7b3 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/region/service/impl/RegionQueryServiceImpl.java @@ -0,0 +1,50 @@ +package com.dev.moim.domain.region.service.impl; + +import com.dev.moim.domain.region.dto.RegionListDTO; +import com.dev.moim.domain.region.dto.RegionSearchListDTO; +import com.dev.moim.domain.region.entity.Dong; +import com.dev.moim.domain.region.repository.DongRepository; +import com.dev.moim.domain.region.repository.SidoRepository; +import com.dev.moim.domain.region.repository.SigunguRepository; +import com.dev.moim.domain.region.service.RegionQueryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RegionQueryServiceImpl implements RegionQueryService { + + private final SidoRepository sidoRepository; + private final SigunguRepository sigunguRepository; + private final DongRepository dongRepository; + + @Override + public RegionListDTO getSido() { + return RegionListDTO.toSido(sidoRepository.findAll()); + } + + @Override + public RegionListDTO getSigungu(Long parentId) { + return RegionListDTO.toSigungu(sigunguRepository.findByParentId(parentId)); + } + + @Override + public RegionListDTO getDong(Long parentId) { + return RegionListDTO.toDong(dongRepository.findByParentId(parentId)); + } + + @Override + public RegionSearchListDTO getRegions(String searchTerm, Long cursor, Integer take) { + Pageable pageable = PageRequest.of(0, take); + Slice dongSlice = dongRepository.searchAddress(searchTerm, cursor, pageable); + + return RegionSearchListDTO.of(dongSlice); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/controller/AlarmController.java b/src/main/java/com/dev/moim/domain/user/controller/AlarmController.java new file mode 100644 index 00000000..5a8eaaa3 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/controller/AlarmController.java @@ -0,0 +1,45 @@ +package com.dev.moim.domain.user.controller; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.dto.AlarmCountResponseDTO; +import com.dev.moim.domain.user.dto.EventDTO; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.firebase.service.FcmService; +import com.dev.moim.global.security.annotation.AuthUser; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/alarms") +@Tag(name = "알림 관련 컨트롤러") +public class AlarmController { + + private final FcmService fcmService; + private final UserQueryService userQueryService; + + @Operation(summary = "이벤트 알림 발송", description = "모든 유저에게 이벤트 알림 발송을 합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @PostMapping("/event") + public BaseResponse sendEventAlarm(@AuthUser User user, @RequestBody EventDTO eventDTO) { + fcmService.sendEventAlarm(user, eventDTO); + return BaseResponse.onSuccess("이벤트 알림 보내기에 성공하였습니다."); + } + + @Operation(summary = "알림 남은 숫자 조회", description = "알림 남은 숫자 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping("/alarms/count") + public BaseResponse sendEventAlarm(@AuthUser User user) { + Integer count = userQueryService.countAlarm(user); + return BaseResponse.onSuccess(AlarmCountResponseDTO.toAlarmCountResponseDTO(count)); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/controller/UserController.java b/src/main/java/com/dev/moim/domain/user/controller/UserController.java new file mode 100644 index 00000000..227c6243 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/controller/UserController.java @@ -0,0 +1,461 @@ +package com.dev.moim.domain.user.controller; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.moim.dto.MoimPreviewListDTO; +import com.dev.moim.domain.moim.dto.calender.PlanDetailDTO; +import com.dev.moim.domain.moim.dto.calender.PlanMonthListDTO; +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.domain.user.dto.UserDailyPlanPageDTO; +import com.dev.moim.domain.user.dto.UserPlanDTO; +import com.dev.moim.domain.user.dto.*; +import com.dev.moim.domain.user.service.ReviewCommandService; +import com.dev.moim.domain.user.service.ReviewQueryService; +import com.dev.moim.domain.user.service.UserCommandService; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.security.annotation.AuthUser; +import com.dev.moim.global.validation.annotation.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Validated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/users") +@Tag(name = "유저 관련 컨트롤러") +public class UserController { + + private final UserQueryService userQueryService; + private final UserCommandService userCommandService; + private final CalenderQueryService calenderQueryService; + private final ReviewQueryService reviewQueryService; + private final ReviewCommandService reviewCommandService; + + @Operation(summary = "(멀티 프로필 도입 ver) 유저 프로필 생성", description = "유저의 프로필을 생성하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다.") + }) + @PostMapping("/profile") + public BaseResponse createProfile( + @AuthUser User user, + @Valid @RequestBody CreateProfileDTO request + ) { + userCommandService.createProfile(user, request); + return BaseResponse.onSuccess("유저 프로필 생성 성공"); + } + + // 유저 대표 프로필 조회 + @Operation(summary = "유저 기본 프로필 조회", description = "유저가 기본으로 설정한 프로필 정보를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "USERPROFILE_001", description = "프로필을 찾을 수 없습니다.") + }) + @GetMapping("/profile") + public BaseResponse getProfile( + @AuthUser User user + ) { + return BaseResponse.onSuccess(userQueryService.getProfile(user)); + } + + // 유저 프로필 리스트 조회 + @Operation(summary = "(멀티 프로필 도입 ver) 유저 프로필 리스트 조회", description = "유저 프로필 리스트를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/profile/list") + public BaseResponse getUserProfileList( + @AuthUser User user, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(userQueryService.getUserProfileList(user, cursor, take)); + } + + @Operation(summary = "(멀티 프로필 도입 ver) 특정 유저 프로필 사용하는 모임 리스트 조회", description = "유저의 특정 프로필을 사용하는 모임 리스트를 조회하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + @ApiResponse(responseCode = "USERPROFILE_001", description = "프로필을 찾을 수 없습니다."), + @ApiResponse(responseCode = "USERPROFILE_003", description = "해당 유저의 프로필이 아닙니다."), + }) + @GetMapping("/profile/{profileId}/target-moims") + public BaseResponse getUserProfileTargetMoimList( + @AuthUser User user, + @ProfileOwnerValidation @PathVariable Long profileId, + @CheckCursorValidation @RequestParam(name = "cursor") Long cursor, + @CheckTakeValidation @RequestParam(name = "take") Integer take + ) { + return BaseResponse.onSuccess(userQueryService.getUserProfileTargetMoimList(profileId, cursor, take)); + } + + // 유저 멀티 프로필 수정 + // TODO: residence 프로필마다 설정 가능 or default 정보로 + // TODO: 대표 프로필 설정 여부 + @Operation(summary = "(멀티 프로필 도입 ver) 유저 멀티 프로필 수정", description = "유저의 멀티 프로필을 수정하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "USERPROFILE_001", description = "프로필을 찾을 수 없습니다."), + @ApiResponse(responseCode = "USERPROFILE_003", description = "해당 유저의 프로필이 아닙니다."), + }) + @PutMapping("/profile/{profileId}") + public BaseResponse updateUserProfile( + @AuthUser User user, + @ProfileOwnerValidation @PathVariable Long profileId, + @Valid @RequestBody UpdateMultiProfileDTO request + ) { + userCommandService.updateUserProfile(user, profileId, request); + return BaseResponse.onSuccess("멀티 프로필 수정 성공했습니다."); + } + + @Operation(summary = "(멀티 프로필 도입 전 ver) 유저 프로필 수정", description = "(멀티 프로필 도입 전 ver) 유저의 프로필을 수정하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + }) + @PutMapping("/profile") + public BaseResponse updateUserInfo( + @AuthUser User user, + @Valid @RequestBody UpdateUserInfoDTO request + ) { + userCommandService.updateUserInfo(user, request); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "(멀티 프로필 도입 ver) 유저 기본 정보 수정", description = "유저의 기본 정보를 수정하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨"), + }) + @PutMapping("/profile/default-info") + public BaseResponse updateUserDefaultInfo( + @AuthUser User user, + @Valid @RequestBody UpdateUserDefaultInfoDTO request + ) { + userCommandService.updateUserDefaultInfo(user, request); + return BaseResponse.onSuccess(null); + } + + // 특정 프로필 삭제 + // TODO: 해당 프로필이 대표 프로필인 경우 처리 -> 현재 : 에러 처리 + // TODO: 해당 프로필을 사용중인 모임이 있는 경우 처리 -> 현재 : 에러 처리 + @Operation(summary = "(멀티 프로필 도입 ver) 유저 프로필 삭제", description = "유저의 특정 프로필을 삭제하는 기능입니다." + + " 대표 프로필이거나 해당 프로필을 사용중인 모임이 있는 경우 삭제 불가능합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "USERPROFILE_001", description = "프로필을 찾을 수 없습니다."), + @ApiResponse(responseCode = "USERPROFILE_003", description = "해당 유저의 프로필이 아닙니다."), + @ApiResponse(responseCode = "USERPROFILE_004", description = "해당 프로필을 사용 중인 모임이 있습니다."), + @ApiResponse(responseCode = "USERPROFILE_005", description = "대표 프로필은 삭제할 수 없습니다. 대표 프로필을 변경해주세요.") + }) + @DeleteMapping(("/profile/{profileId}")) + public BaseResponse deleteUserProfile( + @AuthUser User user, + @DeletableProfileValidation @PathVariable Long profileId + ) { + userCommandService.deleteUserProfile(profileId); + return BaseResponse.onSuccess("프로필 삭제 성공했습니다."); + } + + // TODO: 특정 프로필 상세 조회로 변경 + @Operation(summary = "유저 프로필 상세 조회", description = "유저의 프로필을 상세 조회하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping(("/profile/detail")) + public BaseResponse getUserDetailProfile( + @AuthUser User user + ) { + return BaseResponse.onSuccess(userQueryService.getDetailProfile(user.getId())); + } + + // TODO: moimId 받는걸로 수정 (해당 모임에서의 프로필 조회) + @Operation(summary = "멤버 프로필 상세 조회", description = "다른 멤버의 프로필을 상세 조회하는 기능입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping(("/profile/detail/{userId}")) + public BaseResponse getMemberDetailProfile( + @ExistUserValidation @PathVariable Long userId + ) { + return BaseResponse.onSuccess(userQueryService.getDetailProfile(userId)); + } + + @Operation(summary = "(멀티 프로필 도입 ver) 특정 모임에서 사용할 프로필 변경", description = "특정 모임에서 사용할 프로필을 변경합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "MOIM_001", description = "모임을 찾을 수 없습니다."), + @ApiResponse(responseCode = "MOIM_003", description = "모임의 멤버가 아닙니다."), + @ApiResponse(responseCode = "USERPROFILE_001", description = "프로필을 찾을 수 없습니다."), + @ApiResponse(responseCode = "USERPROFILE_003", description = "해당 유저의 프로필이 아닙니다.") + }) + @PutMapping(("/profile/moim/{moimId}")) + public BaseResponse updateMoimProfile( + @AuthUser User user, + @UserMoimValidaton @PathVariable Long moimId, + @Valid @RequestBody UpdateMoimProfileDTO request + ) { + userCommandService.updateMoimProfile(user, moimId, request); + return BaseResponse.onSuccess("특정 모임에서 사용할 프로필 변경 성공했습니다"); + } + + @Operation(summary = "유저 후기 리스트 조회", description = "유저 자신의 후기를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping(("/reviews")) + public BaseResponse getUserReviews( + @AuthUser User user, + @CheckPageValidation @RequestParam(name = "page") int page, + @CheckSizeValidation @RequestParam(name = "size") int size + ) { + return BaseResponse.onSuccess(reviewQueryService.getUserReviews(user.getId(), page, size)); + } + + @Operation(summary = "멤버 후기 리스트 조회", description = "다른 유저의 후기를 조회합니다. 조회할 멤버의 id를 넣어주세요.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping(("/reviews/{userId}")) + public BaseResponse getMemberReviews( + @ExistUserValidation @PathVariable Long userId, + @CheckPageValidation @RequestParam(name = "page") int page, + @CheckSizeValidation @RequestParam(name = "size") int size + ) { + return BaseResponse.onSuccess(reviewQueryService.getUserReviews(userId, page, size)); + } + + @Operation(summary = "멤버 후기 작성", description = "유저 본인에겐 후기를 남길 수 없습니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "AUTH_020", description = "존재하지 않는 사용자입니다."), + @ApiResponse(responseCode = "REVIEW_001", description = "유저 본인에게 리뷰를 남길 수 없습니다.") + }) + @PostMapping("/reviews") + public BaseResponse postMemberReview( + @AuthUser User user, + @Valid @RequestBody CreateReviewDTO createReviewDTO) { + return BaseResponse.onSuccess(reviewCommandService.postMemberReview(user, createReviewDTO)); + } + + @Operation(summary = "push 알림 설정", description = "push 알림이 켜져있으면 끄고 꺼져있으면 킵니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @PostMapping("/alarms/push") + public BaseResponse settingPushAlarm(@AuthUser User user) { + return BaseResponse.onSuccess(userCommandService.settingPushAlarm(user)); + } + + @Operation(summary = "event 알림 설정", description = "event 알림이 켜져있으면 끄고 꺼져있으면 킵니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @PostMapping("/alarms/event") + public BaseResponse settingEventAlarm(@AuthUser User user) { + return BaseResponse.onSuccess(userCommandService.settingEventAlarm(user)); + } + + @Operation(summary = "알림 상태 조회", description = "알림 상태를 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping("/alarms/status") + public BaseResponse getAlarmSetting(@AuthUser User user) { + return BaseResponse.onSuccess(AlarmDTO.toAlarmDTO(user)); + } + + @Operation(summary = "개인 일정 추가", description = "개인의 일정을 추가합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON201", description = "요청 성공 및 리소스 생성됨") + }) + @PostMapping("/calender") + public BaseResponse createIndividualPlan( + @AuthUser User user, + @RequestBody CreateIndividualPlanRequestDTO request + ) { + userCommandService.createIndividualPlan(user, request); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "개인 일정 삭제", description = "개인의 일정을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @DeleteMapping("/calender/{individualPlanId}") + public BaseResponse deleteIndividualPlan( + @IndividualPlanValidation @PathVariable Long individualPlanId + ) { + userCommandService.deleteIndividualPlan(individualPlanId); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "개인 일정 수정", description = "개인의 일정을 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @PutMapping("/calender/{individualPlanId}") + public BaseResponse updateIndividualPlan( + @IndividualPlanValidation @PathVariable Long individualPlanId, + @RequestBody CreateIndividualPlanRequestDTO request + ) { + userCommandService.updateIndividualPlan(individualPlanId, request); + return BaseResponse.onSuccess(null); + } + + @Operation(summary = "특정 날짜 (연, 월) : 개인 일정 리스트 조회", description = "유저의 개인 일정들을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "유저 일정 조회 성공"), + }) + @GetMapping("/monthly/individual-plans") + public BaseResponse>> getIndividualPlans( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month + ) { + return BaseResponse.onSuccess(userQueryService.getIndividualPlans(user, year, month)); + } + + @Operation(summary = "특정 날짜 (연, 월) : 유저가 참여 신청한 모임 일정 리스트 조회", description = "유저가 참여 신청한 모임 일정들을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/monthly/moim-plans") + public BaseResponse>> getUserPlans( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month + ) { + return BaseResponse.onSuccess(userQueryService.getUserPlans(user, year, month)); + } + + @Operation(summary = "특정 날짜 (연, 월) : 모든 타입 일정 리스트 조회", description = "유저의 모든 타입 일정들 (참여 신청한 모임 일정 + 개인 일정) 을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "유저 일정 조회 성공"), + }) + @GetMapping("/monthly/total-plans") + public BaseResponse>> getUserMonthlyPlans( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month + ) { + return BaseResponse.onSuccess(userQueryService.getUserMonthlyPlans(user, year, month)); + } + + @Operation(summary = "특정 날짜 (연, 월, 일) : 유저가 참여 신청한 모임 일정 리스트 조회", description = "특정 날짜에 예정된 (유저가 참여 신청한 모임 일정)을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/daily/moim-plans") + public BaseResponse getUserDailyMoimPlans( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month, + @Parameter(description = "일") @RequestParam int day, + @CheckPageValidation @RequestParam(name = "page") int page, + @CheckSizeValidation @RequestParam(name = "size") int size + ) { + return BaseResponse.onSuccess(userQueryService.getUserDailyMoimPlans(user, year, month, day, page, size)); + } + + @Operation(summary = "특정 날짜 (연, 월, 일) : 유저의 개인 일정 리스트 조회", description = "특정 날짜에 예정된 (유저의 개인 일정)을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/daily/individual-plans") + public BaseResponse getUserDailyIndividualPlans( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month, + @Parameter(description = "일") @RequestParam int day, + @CheckPageValidation @RequestParam(name = "page") int page, + @CheckSizeValidation @RequestParam(name = "size") int size + ) { + return BaseResponse.onSuccess(userQueryService.getUserDailyIndividualPlans(user, year, month, day, page, size)); + } + + @Operation(summary = "특정 날짜 (연, 월, 일) : 유저의 (개인 일정 + 모임 신청 일정 + 할당받은 todo) 리스트 조회", description = "특정 날짜에 예정된 (개인 + 모임 신청 일정) 리스트를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/daily/total-plans") + public BaseResponse getUserDailyPlans( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month, + @Parameter(description = "일") @RequestParam int day, + @CheckPageValidation @RequestParam(name = "page") int page, + @CheckSizeValidation @RequestParam(name = "size") int size + ) { + return BaseResponse.onSuccess(userQueryService.getUserDailyPlans(user, year, month, day, page, size)); + } + + @Operation(summary = "특정 날짜 (연, 월, 일) : 유저의 총 일정 개수 조회", description = "특정 날짜에 예정된 유저의 총 일정 개수 (개인 일정 + 모임 신청 일정 + 할당받은 todo) 를 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다.") + }) + @GetMapping("/user-plan-count") + public BaseResponse getUserDailyPlanCnt ( + @AuthUser User user, + @Parameter(description = "연도") @RequestParam int year, + @Parameter(description = "월") @RequestParam int month, + @Parameter(description = "일") @RequestParam int day + ) { + return BaseResponse.onSuccess(userQueryService.getUserDailyPlanCnt(user, year, month, day)); + } + + @Operation(summary = "유저의 개인 일정 상세 조회", description = "유저의 특정 개인 일정을 상세 조회 합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "PLAN_001", description = "존재하지 않는 개인 일정 입니다."), + @ApiResponse(responseCode = "PLAN_002", description = "해당 일정의 작성자가 아닙니다.") + }) + @GetMapping("/individual-plan/{individualPlanId}") + public BaseResponse getIndividualPlanDetail ( + @AuthUser User user, + @IndividualPlanValidation @PathVariable Long individualPlanId + ) { + return BaseResponse.onSuccess(userQueryService.getIndividualPlanDetail(user, individualPlanId)); + } + + @Operation(summary = "유저의 모임 참여 신청 일정 상세 조회", description = "유저의 특정 모임 참여 신청 일정을 상세 조회 합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + @ApiResponse(responseCode = "PLAN_002", description = "해당 일정에 참여 신청하지 않았습니다."), + @ApiResponse(responseCode = "PLAN_005", description = "존재하지 않는 일정입니다.") + }) + @GetMapping("/moim-plan/{userMoimPlanId}") + public BaseResponse getUserMoimPlanDetail ( + @AuthUser User user, + @UserPlanValidation @PathVariable Long userMoimPlanId + ) { + return BaseResponse.onSuccess(calenderQueryService.getPlanDetails(user, userMoimPlanId)); + } + + @Operation(summary = "알림 삭제 API", description = "모든 알림을 삭제합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @DeleteMapping("/alarms") + public BaseResponse deleteAlarms(@AuthUser User user) { + userCommandService.deleteAlarms(user); + return BaseResponse.onSuccess("모든 알림을 삭제하였습니다."); + } + + @Operation(summary = "알림 목록 API", description = "알림 목록을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "COMMON200", description = "성공입니다."), + }) + @GetMapping("/alarms") + public BaseResponse getAlarms(@AuthUser User user, @RequestParam("cursor") Long cursor, @RequestParam("take") Integer take) { + AlarmResponseListDTO alarms = userQueryService.getAlarms(user, cursor, take); + return BaseResponse.onSuccess(alarms); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/AlarmCountResponseDTO.java b/src/main/java/com/dev/moim/domain/user/dto/AlarmCountResponseDTO.java new file mode 100644 index 00000000..a5143a52 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/AlarmCountResponseDTO.java @@ -0,0 +1,9 @@ +package com.dev.moim.domain.user.dto; + +public record AlarmCountResponseDTO( + Integer remainAlarms +) { + public static AlarmCountResponseDTO toAlarmCountResponseDTO(Integer remainAlarms) { + return new AlarmCountResponseDTO(remainAlarms); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/AlarmDTO.java b/src/main/java/com/dev/moim/domain/user/dto/AlarmDTO.java new file mode 100644 index 00000000..23de47eb --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/AlarmDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.User; + +public record AlarmDTO( + Boolean isPushAlarm, + Boolean isEventAlarm +) { + public static AlarmDTO toAlarmDTO(User user) { + return new AlarmDTO(user.getIsPushAlarm(), user.getIsEventAlarm()); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/AlarmResponseDTO.java b/src/main/java/com/dev/moim/domain/user/dto/AlarmResponseDTO.java new file mode 100644 index 00000000..a14e3daf --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/AlarmResponseDTO.java @@ -0,0 +1,21 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.Alarm; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; + +import java.time.LocalDateTime; + +public record AlarmResponseDTO( + Long alarmId, + String title, + String content, + AlarmDetailType alarmDetailType, + Long moimId, + Long postId, + Long commentId, + LocalDateTime createdAt +) { + public static AlarmResponseDTO toAlarmResponseDTO(Alarm alarm) { + return new AlarmResponseDTO(alarm.getId(), alarm.getTitle(), alarm.getContent(), alarm.getAlarmDetailType(), alarm.getMoimId(), alarm.getPostId(), alarm.getCommentId(), alarm.getCreatedAt()); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/AlarmResponseListDTO.java b/src/main/java/com/dev/moim/domain/user/dto/AlarmResponseListDTO.java new file mode 100644 index 00000000..7a65ac7f --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/AlarmResponseListDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.user.dto; + +import java.util.List; + +public record AlarmResponseListDTO( + Long nextCursor, + Boolean hasNext, + List alarmResponseDTOList +) { + public static AlarmResponseListDTO toAlarmResponseListDTO(List alarmResponseDTOList, Long cursor, Boolean hasNext) { + return new AlarmResponseListDTO(cursor, hasNext, alarmResponseDTOList); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ChatRoomUserListResponse.java b/src/main/java/com/dev/moim/domain/user/dto/ChatRoomUserListResponse.java new file mode 100644 index 00000000..6b1da621 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ChatRoomUserListResponse.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.user.dto; + +import java.util.List; + +public record ChatRoomUserListResponse( + List chatRoomUserResponses +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ChatRoomUserResponse.java b/src/main/java/com/dev/moim/domain/user/dto/ChatRoomUserResponse.java new file mode 100644 index 00000000..dad60629 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ChatRoomUserResponse.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; + +public record ChatRoomUserResponse( + String username, + String userProfile +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/CreateIndividualPlanRequestDTO.java b/src/main/java/com/dev/moim/domain/user/dto/CreateIndividualPlanRequestDTO.java new file mode 100644 index 00000000..32e06068 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/CreateIndividualPlanRequestDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.user.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.time.LocalTime; + +public record CreateIndividualPlanRequestDTO( + String title, + LocalDate date, + @Schema(type = "string", example = "12:00:00") + LocalTime startTime, + String location, + String locationDetail, + String memo +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/CreateProfileDTO.java b/src/main/java/com/dev/moim/domain/user/dto/CreateProfileDTO.java new file mode 100644 index 00000000..72e698dc --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/CreateProfileDTO.java @@ -0,0 +1,16 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.global.validation.annotation.UserMoimListValidation; + +import java.util.List; + +public record CreateProfileDTO( + String nickname, + String residence, + String introduction, + String imageKey, + @UserMoimListValidation List targetMoimIdList, + ProfileType profileType +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/CreateReviewDTO.java b/src/main/java/com/dev/moim/domain/user/dto/CreateReviewDTO.java new file mode 100644 index 00000000..a564f284 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/CreateReviewDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.global.validation.annotation.SelfReviewValidation; + +public record CreateReviewDTO( + @SelfReviewValidation Long targetUserId, + Double rating, + String content +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/CreateReviewResultDTO.java b/src/main/java/com/dev/moim/domain/user/dto/CreateReviewResultDTO.java new file mode 100644 index 00000000..b1bfdc7a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/CreateReviewResultDTO.java @@ -0,0 +1,13 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.UserReview; + +public record CreateReviewResultDTO( + Long reviewId +) { + public static CreateReviewResultDTO of(UserReview userReview) { + return new CreateReviewResultDTO( + userReview.getId() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/EventDTO.java b/src/main/java/com/dev/moim/domain/user/dto/EventDTO.java new file mode 100644 index 00000000..245ce5ef --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/EventDTO.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.user.dto; + +public record EventDTO( + String title, + String content +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/JoinedMoimDTO.java b/src/main/java/com/dev/moim/domain/user/dto/JoinedMoimDTO.java new file mode 100644 index 00000000..9207e2dd --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/JoinedMoimDTO.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.moim.entity.enums.MoimCategory; + +public record JoinedMoimDTO( + Long moimId, + String moimImageUrl, + String moimName, + String introduction, + MoimCategory moimCategory, + String location, + int participantCnt +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ProfileDTO.java b/src/main/java/com/dev/moim/domain/user/dto/ProfileDTO.java new file mode 100644 index 00000000..67ed6f33 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ProfileDTO.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.account.entity.enums.Provider; + +public record ProfileDTO( + Long userId, + Long profileId, + ProfileType profileType, + String nickname, + String imageUrl, + Provider provider +) { + public static ProfileDTO of(User user, UserProfile userProfile) { + return new ProfileDTO( + user.getId(), + userProfile.getId(), + userProfile.getProfileType(), + userProfile.getName(), + userProfile.getImageUrl()!= null && !userProfile.getImageUrl().isEmpty() ? userProfile.getImageUrl() : null, + user.getProvider() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ProfileDetailDTO.java b/src/main/java/com/dev/moim/domain/user/dto/ProfileDetailDTO.java new file mode 100644 index 00000000..8876c184 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ProfileDetailDTO.java @@ -0,0 +1,44 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.Gender; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.account.entity.enums.Provider; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public record ProfileDetailDTO( + Long userId, + Long profileId, + ProfileType profileType, + Provider provider, + String imageUrl, + String nickname, + String residence, + LocalDate birth, + Gender gender, + LocalDateTime createdAt, + double rating, + int participateMoimCnt, + String introduction +) { + public static ProfileDetailDTO from(User user, UserProfile userProfile, int participateMoimCnt) { + return new ProfileDetailDTO( + user.getId(), + userProfile.getId(), + userProfile.getProfileType(), + user.getProvider(), + userProfile.getImageUrl()!= null && !userProfile.getImageUrl().isEmpty() ? userProfile.getImageUrl() : null, + userProfile.getName(), + user.getResidence(), + user.getBirth(), + user.getGender(), + userProfile.getCreatedAt(), + user.getRating(), + participateMoimCnt, + userProfile.getIntroduction() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ProfilePageDTO.java b/src/main/java/com/dev/moim/domain/user/dto/ProfilePageDTO.java new file mode 100644 index 00000000..5465b2fa --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ProfilePageDTO.java @@ -0,0 +1,25 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.UserProfile; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record ProfilePageDTO( + List profileList, + Long nextCursor, + Boolean hasNext +) { + public static ProfilePageDTO toProfileListDTO(List profileDTOList, Slice userProfileSlice) { + + Long nextCursor = userProfileSlice.hasNext() && !userProfileSlice.getContent().isEmpty() + ? userProfileSlice.getContent().get(userProfileSlice.getNumberOfElements() - 1).getId() + : null; + + return new ProfilePageDTO( + profileDTOList, + nextCursor, + userProfileSlice.hasNext() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ReviewDTO.java b/src/main/java/com/dev/moim/domain/user/dto/ReviewDTO.java new file mode 100644 index 00000000..8cc61dfe --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ReviewDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.UserReview; + +public record ReviewDTO( + Long reviewId, + String content, + Double rating +) { + public static ReviewDTO of(UserReview userReview) { + return new ReviewDTO( + userReview.getId(), + userReview.getContent(), + userReview.getRating() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/ReviewListDTO.java b/src/main/java/com/dev/moim/domain/user/dto/ReviewListDTO.java new file mode 100644 index 00000000..5f321dbb --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/ReviewListDTO.java @@ -0,0 +1,26 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.UserReview; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record ReviewListDTO( + List reviewDTOList, + Long totalReviewCnt, + Boolean isFirst, + Boolean hasNext +) { + public static ReviewListDTO of(Page userReviewPage) { + List reviewDTOList = userReviewPage.stream() + .map(ReviewDTO::of) + .toList(); + + return new ReviewListDTO( + reviewDTOList, + userReviewPage.getTotalElements(), + userReviewPage.isFirst(), + userReviewPage.hasNext() + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UpdateMoimProfileDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UpdateMoimProfileDTO.java new file mode 100644 index 00000000..3c5ae15d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UpdateMoimProfileDTO.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.global.validation.annotation.ProfileOwnerValidation; + +public record UpdateMoimProfileDTO( + @ProfileOwnerValidation Long profileId +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UpdateMultiProfileDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UpdateMultiProfileDTO.java new file mode 100644 index 00000000..31a28a41 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UpdateMultiProfileDTO.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.global.validation.annotation.UserMoimListValidation; +import jakarta.validation.constraints.NotBlank; + +import java.util.List; + +public record UpdateMultiProfileDTO( + @NotBlank String nickname, + String imageKey, + String introduction, + @UserMoimListValidation List targetMoimIdList +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UpdateUserDefaultInfoDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UpdateUserDefaultInfoDTO.java new file mode 100644 index 00000000..66a5a3cf --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UpdateUserDefaultInfoDTO.java @@ -0,0 +1,12 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.enums.Gender; + +import java.time.LocalDate; + +public record UpdateUserDefaultInfoDTO( + String residence, + Gender gender, + LocalDate birth +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UpdateUserInfoDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UpdateUserInfoDTO.java new file mode 100644 index 00000000..90fbfddd --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UpdateUserInfoDTO.java @@ -0,0 +1,21 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.account.entity.enums.Gender; +import com.dev.moim.global.validation.annotation.UserMoimListValidation; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +import java.time.LocalDate; +import java.util.List; + +public record UpdateUserInfoDTO( + @NotBlank String nickname, + String imageKey, + String residence, + Gender gender, + LocalDate birth, + String introduction, + @Schema(description = "프로필에 공개할 모임 ID 리스트") + @UserMoimListValidation List publicMoimList +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UserDailyPlanCntDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UserDailyPlanCntDTO.java new file mode 100644 index 00000000..8f458506 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UserDailyPlanCntDTO.java @@ -0,0 +1,7 @@ +package com.dev.moim.domain.user.dto; + +public record UserDailyPlanCntDTO( + String nickname, + int dailyPlanCnt +) { +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UserDailyPlanPageDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UserDailyPlanPageDTO.java new file mode 100644 index 00000000..03c2ef81 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UserDailyPlanPageDTO.java @@ -0,0 +1,71 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.moim.entity.IndividualPlan; +import com.dev.moim.domain.moim.entity.Plan; +import com.dev.moim.domain.moim.entity.enums.PlanType; +import com.dev.moim.global.util.TimeUtil; +import org.springframework.data.domain.Slice; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +public record UserDailyPlanPageDTO( + Boolean isFirst, + Boolean hasNext, + List userPlanDTOList +) { + public static UserDailyPlanPageDTO toUserMoimPlan(Slice userMoimPlanSlice, List userPlanDTOList) { + return new UserDailyPlanPageDTO( + userMoimPlanSlice.isFirst(), + userMoimPlanSlice.hasNext(), + userPlanDTOList + ); + } + + public static UserDailyPlanPageDTO toUserIndividualPlan(Slice individualPlanSlice) { + List userIndividualPlanDTOList = individualPlanSlice.stream() + .map(UserPlanDTO::toIndividualPlan) + .toList(); + + return new UserDailyPlanPageDTO( + individualPlanSlice.isFirst(), + individualPlanSlice.hasNext(), + userIndividualPlanDTOList + ); + } + + public static UserDailyPlanPageDTO toUserDailyPlan(List userDailyPlanList, boolean isFirst, boolean hasNext) { + List userDailyPlanDTOList = userDailyPlanList.stream().map(record -> { + Long planId = (Long) record[0]; + String title = (String) record[1]; + Timestamp timestamp = (Timestamp) record[2]; + LocalDateTime time = TimeUtil.toLocalDateTime(timestamp); + String location = (String) record[3]; + String locationDetail = (String) record[4]; + String memo = (String) record[5]; + Long moimId = (Long) record[6]; + String moimName = (String) record[7]; + String planTypeString = (String) record[8]; + PlanType planType = PlanType.fromString(planTypeString); + + return UserPlanDTO.toDailyUserPlan( + planId, + title, + time, + location, + locationDetail, + memo, + moimId, + moimName, + planType + ); + }).toList(); + + return new UserDailyPlanPageDTO( + isFirst, + hasNext, + userDailyPlanDTOList + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/domain/user/dto/UserPlanDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UserPlanDTO.java new file mode 100644 index 00000000..72edb0c2 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UserPlanDTO.java @@ -0,0 +1,93 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.moim.entity.IndividualPlan; +import com.dev.moim.domain.moim.entity.Plan; +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.entity.enums.PlanType; + +import java.time.LocalDateTime; + +import static com.dev.moim.domain.moim.entity.enums.PlanType.*; + +public record UserPlanDTO( + Long planId, + String title, + LocalDateTime time, + String location, + String locationDetail, + String memo, + Long moimId, + String moimName, + PlanType planType, + MoimRole moimRole +) { + public static UserPlanDTO toIndividualPlan(IndividualPlan individualPlan) { + return new UserPlanDTO( + individualPlan.getId(), + individualPlan.getTitle(), + individualPlan.getDate(), + individualPlan.getLocation(), + individualPlan.getLocationDetail(), + individualPlan.getMemo(), + null, + null, + INDIVIDUAL_PLAN, + null + ); + } + + public static UserPlanDTO toUserMoimPlan(Plan plan, UserMoim userMoim) { + return new UserPlanDTO( + plan.getId(), + plan.getTitle(), + plan.getDate(), + plan.getLocation(), + plan.getLocationDetail(), + null, + plan.getMoim().getId(), + plan.getMoim().getName(), + MOIM_PLAN, + userMoim.getMoimRole() + ); + } + + public static UserPlanDTO toUserMoimTodo(Todo todo) { + return new UserPlanDTO( + todo.getId(), + todo.getTitle(), + todo.getDueDate(), + null, + null, + null, + todo.getMoim().getId(), + todo.getMoim().getName(), + TODO_PLAN, + null + ); + } + + public static UserPlanDTO toDailyUserPlan(Long planId, + String title, + LocalDateTime time, + String location, + String locationDetail, + String memo, + Long moimId, + String moimName, + PlanType planType) { + return new UserPlanDTO( + planId, + title, + time, + location, + locationDetail, + memo, + moimId, + moimName, + planType, + null + ); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UserPreviewDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UserPreviewDTO.java new file mode 100644 index 00000000..0acb4410 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UserPreviewDTO.java @@ -0,0 +1,17 @@ +package com.dev.moim.domain.user.dto; + +import com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.moim.entity.enums.MoimRole; + +public record UserPreviewDTO( + Long userId, + String nickname, + String imageKeyName, + MoimRole moimRole +) { + public static UserPreviewDTO toUserPreviewDTO (UserProfileDTO userProfileDTO) { + UserProfile userProfile = userProfileDTO.getUserProfile(); + return new UserPreviewDTO(userProfile.getUser().getId(), userProfile.getName(), userProfile.getImageUrl(), userProfileDTO.getUserMoim().getMoimRole()); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/dto/UserPreviewListDTO.java b/src/main/java/com/dev/moim/domain/user/dto/UserPreviewListDTO.java new file mode 100644 index 00000000..0cad9894 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/dto/UserPreviewListDTO.java @@ -0,0 +1,14 @@ +package com.dev.moim.domain.user.dto; + +import java.util.List; + +public record UserPreviewListDTO( + List userPreviewDTOList, + Boolean hasNext, + Long nextCursor +) { + + public static UserPreviewListDTO toUserPreviewListDTO(List userPreviewDTOList, Boolean hasNext, Long nextCursor) { + return new UserPreviewListDTO(userPreviewDTOList, hasNext, nextCursor); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/service/ReviewCommandService.java b/src/main/java/com/dev/moim/domain/user/service/ReviewCommandService.java new file mode 100644 index 00000000..162741a8 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/ReviewCommandService.java @@ -0,0 +1,10 @@ +package com.dev.moim.domain.user.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.dto.CreateReviewDTO; +import com.dev.moim.domain.user.dto.CreateReviewResultDTO; + +public interface ReviewCommandService { + + CreateReviewResultDTO postMemberReview(User user, CreateReviewDTO request); +} diff --git a/src/main/java/com/dev/moim/domain/user/service/ReviewQueryService.java b/src/main/java/com/dev/moim/domain/user/service/ReviewQueryService.java new file mode 100644 index 00000000..8603ef22 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/ReviewQueryService.java @@ -0,0 +1,8 @@ +package com.dev.moim.domain.user.service; + +import com.dev.moim.domain.user.dto.ReviewListDTO; + +public interface ReviewQueryService { + + ReviewListDTO getUserReviews(Long userId, int page, int size); +} diff --git a/src/main/java/com/dev/moim/domain/user/service/UserCommandService.java b/src/main/java/com/dev/moim/domain/user/service/UserCommandService.java new file mode 100644 index 00000000..0dc03598 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/UserCommandService.java @@ -0,0 +1,35 @@ +package com.dev.moim.domain.user.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.dto.*; + +public interface UserCommandService { + + void createProfile(User user, CreateProfileDTO request); + + void updateUserProfile(User user, Long profileId, UpdateMultiProfileDTO request); + + void deleteUserProfile(Long profileId); + + void updateUserInfo(User user, UpdateUserInfoDTO request); + + void updateUserDefaultInfo(User user, UpdateUserDefaultInfoDTO request); + + void updateMoimProfile(User user, Long moimId, UpdateMoimProfileDTO request); + + AlarmDTO settingPushAlarm(User user); + + AlarmDTO settingEventAlarm(User user); + + void createIndividualPlan(User user, CreateIndividualPlanRequestDTO request); + + void deleteIndividualPlan(Long individualPlanId); + + void updateIndividualPlan(Long individualPlanId, CreateIndividualPlanRequestDTO request); + + void fcmSignOut(User user); + + void notDeadLockFcmSignOut(User user); + + void deleteAlarms(User user); +} diff --git a/src/main/java/com/dev/moim/domain/user/service/UserQueryService.java b/src/main/java/com/dev/moim/domain/user/service/UserQueryService.java new file mode 100644 index 00000000..0b627d28 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/UserQueryService.java @@ -0,0 +1,71 @@ +package com.dev.moim.domain.user.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.domain.moim.dto.MoimPreviewListDTO; +import com.dev.moim.domain.moim.dto.calender.PlanMonthListDTO; +import com.dev.moim.domain.moim.entity.IndividualPlan; +import com.dev.moim.domain.user.dto.UserDailyPlanPageDTO; +import com.dev.moim.domain.user.dto.UserPlanDTO; +import com.dev.moim.domain.user.dto.*; + +import java.util.List; +import java.util.Optional; + +public interface UserQueryService { + + ProfileDTO getProfile(User user); + + ProfilePageDTO getUserProfileList(User user, Long cursor, Integer take); + + MoimPreviewListDTO getUserProfileTargetMoimList(Long profileId, Long cursor, Integer take); + + ProfileDetailDTO getDetailProfile(Long userId); + + PlanMonthListDTO> getIndividualPlans(User user, int year, int month); + + PlanMonthListDTO> getUserPlans(User user, int year, int month); + + PlanMonthListDTO> getUserMonthlyPlans(User user, int year, int month); + + UserDailyPlanPageDTO getUserDailyMoimPlans(User user, int year, int month, int day, int page, int size); + + UserDailyPlanPageDTO getUserDailyIndividualPlans(User user, int year, int month, int day, int page, int size); + + UserDailyPlanPageDTO getUserDailyPlans(User user, int year, int month, int day, int page, int size); + + UserDailyPlanCntDTO getUserDailyPlanCnt(User user, int year, int month, int day); + + UserPlanDTO getIndividualPlanDetail(User user, Long individualPlanId); + + UserPlanDTO getUserMoimPlanDetail(User user, Long userMoimPlanId); + + List findUserMoimIdListByUserId(Long userId); + + Optional findUserByPlanId(Long individualPlanId); + + boolean isExistEmail(String email); + + boolean isMoimOwner(User user); + + List findAllUser(); + + Optional findUserById(Long userId); + + AlarmResponseListDTO getAlarms(User user, Long cursor, Integer take); + + List findUnReadUserListByPost(User user, Long moimId, Long postId); + + boolean existsByProviderAndProviderId(Provider provider, String providerId); + + boolean existsByEmail(String email); + + Integer countAlarm(User user); + + Optional findUserProfile(Long profileId); + + boolean existsByUserProfileIdAndJoinStatus(Long profileId); + +// ChatRoomUserListResponse getUserByChatRoom(User user, Long chatRoomId); +} diff --git a/src/main/java/com/dev/moim/domain/user/service/impl/ReviewCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/user/service/impl/ReviewCommandServiceImpl.java new file mode 100644 index 00000000..ce8b022a --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/impl/ReviewCommandServiceImpl.java @@ -0,0 +1,62 @@ +package com.dev.moim.domain.user.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserReview; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.account.repository.UserReviewRepository; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.user.dto.CreateReviewDTO; +import com.dev.moim.domain.user.dto.CreateReviewResultDTO; +import com.dev.moim.domain.user.service.ReviewCommandService; +import com.dev.moim.global.error.handler.UserException; +import com.dev.moim.global.firebase.service.FcmService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_NOT_FOUND; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class ReviewCommandServiceImpl implements ReviewCommandService { + + private final UserReviewRepository userReviewRepository; + private final UserRepository userRepository; + private final AlarmService alarmService; + private final FcmService fcmService; + + @Override + public CreateReviewResultDTO postMemberReview(User user, CreateReviewDTO request) { + + User targetUser = userRepository.findById(request.targetUserId()) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + + UserReview userReview = UserReview.builder() + .rating(request.rating()) + .content(request.content()) + .user(targetUser) + .writerId(user.getId()) + .build(); + userReviewRepository.save(userReview); + + double averageRating = userReviewRepository.findAllByUserId(targetUser.getId()) + .stream() + .mapToDouble(UserReview::getRating) + .average() + .orElse(0.0); + + targetUser.updateRating(averageRating); + + if (targetUser.getIsPushAlarm()) { + alarmService.saveAlarm(user, targetUser, "후기가 도착했습니다" , "새로 도착한 후기를 확인해주세요", AlarmType.PUSH, AlarmDetailType.REVIEW, null, null, null); + fcmService.sendPushNotification(targetUser, "후기가 도착했습니다" , "새로 도착한 후기를 확인해주세요", AlarmDetailType.REVIEW); + } + + return CreateReviewResultDTO.of(userReview); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/service/impl/ReviewQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/user/service/impl/ReviewQueryServiceImpl.java new file mode 100644 index 00000000..0164022d --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/impl/ReviewQueryServiceImpl.java @@ -0,0 +1,31 @@ +package com.dev.moim.domain.user.service.impl; + +import com.dev.moim.domain.account.entity.UserReview; +import com.dev.moim.domain.account.repository.UserReviewRepository; +import com.dev.moim.domain.user.dto.ReviewListDTO; +import com.dev.moim.domain.user.service.ReviewQueryService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class ReviewQueryServiceImpl implements ReviewQueryService { + + private final UserReviewRepository userReviewRepository; + + @Override + public ReviewListDTO getUserReviews(Long userId, int page, int size) { + PageRequest pageRequest = PageRequest.of(page-1, size, Sort.by(Sort.Direction.ASC, "id")); + Page userReviewPage = userReviewRepository.findByUserId(userId, pageRequest); + + return ReviewListDTO.of(userReviewPage); + } + +} diff --git a/src/main/java/com/dev/moim/domain/user/service/impl/UserCommandServiceImpl.java b/src/main/java/com/dev/moim/domain/user/service/impl/UserCommandServiceImpl.java new file mode 100644 index 00000000..1d831416 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/impl/UserCommandServiceImpl.java @@ -0,0 +1,188 @@ +package com.dev.moim.domain.user.service.impl; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.account.repository.AlarmRepository; +import com.dev.moim.domain.account.repository.UserProfileRepository; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.moim.entity.IndividualPlan; +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.repository.IndividualPlanRepository; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import com.dev.moim.domain.user.dto.*; +import com.dev.moim.domain.user.service.UserCommandService; +import com.dev.moim.global.error.handler.IndividualPlanException; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.UserException; +import com.dev.moim.global.s3.service.S3Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static com.dev.moim.domain.moim.entity.enums.ProfileStatus.PRIVATE; +import static com.dev.moim.domain.moim.entity.enums.ProfileStatus.PUBLIC; +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class UserCommandServiceImpl implements UserCommandService { + + private final UserProfileRepository userProfileRepository; + private final UserMoimRepository userMoimRepository; + private final UserRepository userRepository; + private final S3Service s3Service; + private final IndividualPlanRepository individualPlanRepository; + private final AlarmRepository alarmRepository; + + @Override + public void createProfile(User user, CreateProfileDTO request) { + + if (request.profileType() == ProfileType.MAIN) { + UserProfile mainProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), ProfileType.MAIN) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND_MAIN)); + + mainProfile.updateProfileType(ProfileType.SUB); + } + + UserProfile userProfile = UserProfile.builder() + .user(user) + .name(request.nickname()) + .introduction(request.introduction()) + .imageUrl(request.imageKey() != null && !request.imageKey().isEmpty() ? s3Service.generateStaticUrl(request.imageKey()) : null) + .profileType(request.profileType()) + .build(); + + userProfileRepository.save(userProfile); + + userMoimRepository.findAllByUserIdAndMoimIdList(user.getId(), request.targetMoimIdList()) + .forEach(userMoim -> userMoim.updateUserProfile(userProfile)); + } + + @Override + public void updateUserProfile(User user, Long profileId, UpdateMultiProfileDTO request) { + + UserProfile userProfile = userProfileRepository.findById(profileId) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + userProfile.updateUserProfile( + request.nickname(), + request.introduction(), + request.imageKey() != null && !request.imageKey().isEmpty()? s3Service.generateStaticUrl(request.imageKey()) : null + ); + + userMoimRepository.findAllByUserIdAndMoimIdListAndJoinStatus(user.getId(), request.targetMoimIdList(), JoinStatus.COMPLETE) + .forEach(userMoim -> userMoim.updateUserProfile(userProfile)); + } + + @Override + public void deleteUserProfile(Long profileId) { + userProfileRepository.deleteById(profileId); + } + + @Override + public void updateUserInfo(User user, UpdateUserInfoDTO request) { + + user.updateUserInfo( + request.residence(), + request.gender(), + request.birth()); + + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), ProfileType.MAIN) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + userProfile.updateUserProfile( + request.nickname(), + request.introduction(), + request.imageKey() != null && !request.imageKey().isEmpty()? s3Service.generateStaticUrl(request.imageKey()) : null + ); + + userMoimRepository.findByUserId(user.getId()).forEach(userMoim -> + userMoim.updateProfileStatus(request.publicMoimList().contains(userMoim.getMoim().getId()) ? PUBLIC : PRIVATE) + ); + } + + @Override + public void updateUserDefaultInfo(User user, UpdateUserDefaultInfoDTO request) { + user.updateUserInfo(request.residence(), request.gender(), request.birth()); + } + + @Override + public void updateMoimProfile(User user, Long moimId, UpdateMoimProfileDTO request) { + + UserMoim userMoim = userMoimRepository.findByUserIdAndMoimId(user.getId(), moimId, JoinStatus.COMPLETE) + .orElseThrow(() -> new MoimException(INVALID_MOIM_MEMBER)); + + UserProfile userProfile = userProfileRepository.findById(request.profileId()) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + userMoim.updateUserProfile(userProfile); + } + + @Override + public AlarmDTO settingPushAlarm(User user) { + user.changePushAlarm(); + return AlarmDTO.toAlarmDTO(user); + } + + @Override + public AlarmDTO settingEventAlarm(User user) { + user.changeEventAlarm(); + return AlarmDTO.toAlarmDTO(user); + } + + @Override + public void createIndividualPlan(User user, CreateIndividualPlanRequestDTO request) { + IndividualPlan individualPlan = IndividualPlan.builder() + .title(request.title()) + .location(request.location()) + .locationDetail(request.locationDetail()) + .memo(request.memo()) + .date(LocalDateTime.of(request.date(), request.startTime())) + .user(user) + .build(); + + individualPlanRepository.save(individualPlan); + } + + @Override + public void deleteIndividualPlan(Long individualPlanId) { + individualPlanRepository.deleteById(individualPlanId); + } + + @Override + public void updateIndividualPlan(Long individualPlanId, CreateIndividualPlanRequestDTO request) { + IndividualPlan individualPlan = individualPlanRepository.findById(individualPlanId) + .orElseThrow(() -> new IndividualPlanException(INDIVIDUAL_PLAN_NOT_FOUND)); + + individualPlan.updateIndividualPlan( + request.title(), + LocalDateTime.of(request.date(), request.startTime()), + request.location(), + request.locationDetail(), + request.memo() + ); + } + + @Override + public void fcmSignOut(User user) { + user.fcmSignOut(); + userRepository.save(user); + } + + @Override + public void notDeadLockFcmSignOut(User user) { + userRepository.updateFcmTokenByUser(user); + } + + @Override + public void deleteAlarms(User user) { + alarmRepository.deleteAllByUser(user); + } +} diff --git a/src/main/java/com/dev/moim/domain/user/service/impl/UserQueryServiceImpl.java b/src/main/java/com/dev/moim/domain/user/service/impl/UserQueryServiceImpl.java new file mode 100644 index 00000000..7d118098 --- /dev/null +++ b/src/main/java/com/dev/moim/domain/user/service/impl/UserQueryServiceImpl.java @@ -0,0 +1,367 @@ +package com.dev.moim.domain.user.service.impl; + +import com.dev.moim.domain.account.entity.Alarm; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.domain.account.repository.AlarmRepository; +import com.dev.moim.domain.account.repository.UserProfileRepository; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.moim.dto.MoimPreviewDTO; +import com.dev.moim.domain.moim.dto.MoimPreviewListDTO; +import com.dev.moim.domain.moim.dto.calender.PlanMonthListDTO; +import com.dev.moim.domain.moim.entity.*; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.PostType; +import com.dev.moim.domain.moim.repository.*; +import com.dev.moim.domain.moim.service.impl.dto.UserProfileDTO; +import com.dev.moim.domain.user.dto.UserDailyPlanPageDTO; +import com.dev.moim.domain.user.dto.UserPlanDTO; +import com.dev.moim.domain.user.dto.*; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.dev.moim.domain.account.entity.enums.ProfileType.MAIN; +import static com.dev.moim.domain.moim.entity.enums.MoimRole.OWNER; +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserQueryServiceImpl implements UserQueryService { + + private final UserProfileRepository userProfileRepository; + private final UserRepository userRepository; + private final UserMoimRepository userMoimRepository; + private final IndividualPlanRepository individualPlanRepository; + private final PlanRepository planRepository; + private final AlarmRepository alarmRepository; + private final UserPlanRepository userPlanRepository; + private final MoimRepository moimRepository; + private final PostRepository postRepository; + private final UserTodoRepository userTodoRepository; + + @Override + public ProfileDTO getProfile(User user) { + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), MAIN) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + return ProfileDTO.of(user, userProfile); + } + + @Override + public ProfilePageDTO getUserProfileList(User user, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? 0L : cursor; + Pageable pageable = PageRequest.of(0, take); + + Slice userProfileSlice = userProfileRepository.findAllByUserIdAndCursor(user.getId(), startCursor, pageable); + + List profileDTOList = userProfileSlice.stream() + .map(userProfile -> { + return ProfileDTO.of(user, userProfile); + }) + .toList(); + + return ProfilePageDTO.toProfileListDTO(profileDTOList, userProfileSlice); + } + + @Override + public MoimPreviewListDTO getUserProfileTargetMoimList(Long profileId, Long cursor, Integer take) { + + Long startCursor = (cursor == 1) ? 0L : cursor; + Pageable pageable = PageRequest.of(0, take); + + Slice userMoimSlice = userMoimRepository.findAllByUserProfileIdAndJoinStatus(profileId, JoinStatus.COMPLETE, startCursor, pageable); + List moimPreviewDTOList = userMoimSlice.stream().map(userMoim -> { + return MoimPreviewDTO.toMoimPreviewDTO(userMoim.getMoim(), userMoim.getMoim().getImageUrl()!= null && !userMoim.getMoim().getImageUrl().isEmpty() ? userMoim.getMoim().getImageUrl() : null); + }).toList(); + + Long nextCursor = userMoimSlice.hasNext() && !userMoimSlice.getContent().isEmpty() + ? userMoimSlice.getContent().get(userMoimSlice.getNumberOfElements() - 1).getId() + : null; + + return MoimPreviewListDTO.toMoimPreviewListDTO(moimPreviewDTOList, nextCursor, userMoimSlice.hasNext()); + } + + @Override + public ProfileDetailDTO getDetailProfile(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(userId, MAIN) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + int participateMoimCnt = userMoimRepository.countByUserIdAndJoinStatus(userId, JoinStatus.COMPLETE); + + return ProfileDetailDTO.from(user, userProfile, participateMoimCnt); + } + + @Override + public UserDailyPlanPageDTO getUserDailyMoimPlans(User user, int year, int month, int day, int page, int size) { + LocalDateTime startOfDay = LocalDate.of(year, month, day).atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1).minusNanos(1); + + Pageable pageable = PageRequest.of(page-1, size); + Slice userMoimPlanSlice = planRepository.findByUserAndDateBetween(user, startOfDay, endOfDay, pageable); + List userPlanDTOList = userMoimPlanSlice.map(plan -> { + UserMoim userMoim = userMoimRepository.findByUserAndMoim(user, plan.getMoim()) + .orElseThrow(() -> new MoimException(USER_MOIM_NOT_FOUND)); + return UserPlanDTO.toUserMoimPlan(plan, userMoim); + }).toList(); + + return UserDailyPlanPageDTO.toUserMoimPlan(userMoimPlanSlice, userPlanDTOList); + } + + @Override + public UserDailyPlanPageDTO getUserDailyIndividualPlans(User user, int year, int month, int day, int page, int size) { + LocalDateTime startOfDay = LocalDate.of(year, month, day).atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1).minusNanos(1); + + Pageable pageable = PageRequest.of(page-1, size, Sort.by(Sort.Direction.ASC, "date")); + Slice userIndividualPlanSlice = individualPlanRepository.findByUserAndDateBetween(user, startOfDay, endOfDay, pageable); + + return UserDailyPlanPageDTO.toUserIndividualPlan(userIndividualPlanSlice); + } + + @Override + public UserDailyPlanPageDTO getUserDailyPlans(User user, int year, int month, int day, int page, int size) { + LocalDateTime startOfDay = LocalDate.of(year, month, day).atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1).minusNanos(1); + + int offset = (page-1) * size; + List userDailyPlanList = planRepository.findUserPlansAndIndividualPlans(user.getId(), startOfDay, endOfDay, size+1, offset); + boolean hasNext = userDailyPlanList.size() > size; + List finalList = hasNext ? userDailyPlanList.subList(0, size) : userDailyPlanList; + boolean isFirst = page == 1; + + return UserDailyPlanPageDTO.toUserDailyPlan(finalList, isFirst, hasNext); + } + + @Override + public PlanMonthListDTO> getIndividualPlans(User user, int year, int month) { + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime startDate = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59); + + List individualPlanList = individualPlanRepository.findByUserIdAndDateBetween(user.getId(), startDate, endDate); + + Map> monthPlanListByDay = individualPlanList.stream() + .map(UserPlanDTO::toIndividualPlan) + .collect(Collectors.groupingBy(dto -> dto.time().getDayOfMonth())); + + return new PlanMonthListDTO<>(monthPlanListByDay); + } + + @Override + public PlanMonthListDTO> getUserPlans(User user, int year, int month) { + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime startDate = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59); + + List userPlanList = userPlanRepository.findByUserIdAndPlanDateBetween(user.getId(), startDate, endDate); + + Map> planListByDay = userPlanList.stream() + .map(userPlan -> { + UserMoim userMoim = userMoimRepository.findByUserAndMoim(user, userPlan.getPlan().getMoim()) + .orElseThrow(() -> new MoimException(USER_MOIM_NOT_FOUND)); + return UserPlanDTO.toUserMoimPlan(userPlan.getPlan(), userMoim); + }).collect(Collectors.groupingBy(dto -> dto.time().getDayOfMonth())); + + return new PlanMonthListDTO<>(planListByDay); + } + + @Override + public PlanMonthListDTO> getUserMonthlyPlans(User user, int year, int month) { + YearMonth yearMonth = YearMonth.of(year, month); + LocalDateTime startDate = yearMonth.atDay(1).atStartOfDay(); + LocalDateTime endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59); + + List userMoimPlanDTOList = userPlanRepository.findByUserIdAndPlanDateBetween(user.getId(), startDate, endDate).stream() + .map(userPlan -> { + UserMoim userMoim = userMoimRepository.findByUserAndMoim(user, userPlan.getPlan().getMoim()) + .orElseThrow(() -> new MoimException(USER_MOIM_NOT_FOUND)); + return UserPlanDTO.toUserMoimPlan(userPlan.getPlan(), userMoim); + }).toList(); + + List individualPlanDTOList = individualPlanRepository.findByUserIdAndDateBetween(user.getId(), startDate, endDate).stream() + .map(UserPlanDTO::toIndividualPlan) + .toList(); + + List userMoimTodoDTOList = userTodoRepository.findByUserIdAndTodoDueDateBetween(user.getId(), startDate, endDate).stream() + .map(userTodo -> UserPlanDTO.toUserMoimTodo(userTodo.getTodo())) + .toList(); + + List userMonthlyPlanDTOList = Stream.concat( + Stream.concat(userMoimPlanDTOList.stream(), individualPlanDTOList.stream()), + userMoimTodoDTOList.stream() + ).toList(); + + Map> userMonthlyPlanListByDay = userMonthlyPlanDTOList.stream() + .collect(Collectors.groupingBy(plan -> plan.time().getDayOfMonth())); + + return new PlanMonthListDTO<>(userMonthlyPlanListByDay); + } + + @Override + public UserDailyPlanCntDTO getUserDailyPlanCnt(User user, int year, int month, int day) { + LocalDateTime startOfDay = LocalDate.of(year, month, day).atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1).minusNanos(1); + + int individualPlanCnt = individualPlanRepository.countByUserAndDateBetween(user, startOfDay, endOfDay); + int moimPlanCnt = userPlanRepository.countPlansByUserAndDateBetween(user, startOfDay, endOfDay); + int todoPlanCnt = userTodoRepository.countByUserAndTodoDueDateBetween(user, startOfDay, endOfDay); + + UserProfile userProfile = userProfileRepository.findByUserIdAndProfileType(user.getId(), MAIN) + .orElseThrow(() -> new UserException(USER_PROFILE_NOT_FOUND)); + + return new UserDailyPlanCntDTO(userProfile.getName(), individualPlanCnt + moimPlanCnt + todoPlanCnt); + } + + @Override + public UserPlanDTO getIndividualPlanDetail(User user, Long individualPlanId) { + IndividualPlan individualPlan = individualPlanRepository.findById(individualPlanId) + .orElseThrow(() -> new PlanException(INDIVIDUAL_PLAN_NOT_FOUND)); + + return UserPlanDTO.toIndividualPlan(individualPlan); + } + + @Override + public UserPlanDTO getUserMoimPlanDetail(User user, Long userMoimPlanId) { + UserPlan userPlan = userPlanRepository.findByUserIdAndPlanId(user.getId(), userMoimPlanId) + .orElseThrow(() -> new PlanException(PLAN_NOT_FOUND)); + + UserMoim userMoim = userMoimRepository.findByUserAndMoim(user, userPlan.getPlan().getMoim()) + .orElseThrow(() -> new MoimException(USER_MOIM_NOT_FOUND)); + + return UserPlanDTO.toUserMoimPlan(userPlan.getPlan(), userMoim); + } + + @Override + public List findUserMoimIdListByUserId(Long userId) { + return userMoimRepository.findByUserId(userId).stream() + .map(userMoim -> userMoim.getMoim().getId()) + .toList(); + } + + @Override + public Optional findUserByPlanId(Long individualPlanId) { + return individualPlanRepository.findById(individualPlanId); + } + + @Override + public boolean isExistEmail(String email) { + return userRepository.existsByEmail(email); + } + + @Override + public boolean isMoimOwner(User user) { + return userMoimRepository.existsByUserAndMoimRole(user, OWNER); + } + + @Override + public List findAllUser() {return userRepository.findAll();} + + @Override + public Optional findUserById(Long userId) { + return userRepository.findById(userId); + } + + @Override + @Transactional + public AlarmResponseListDTO getAlarms(User user, Long cursor, Integer take) { + if (cursor == 1) { + cursor = Long.MAX_VALUE; + } + + userRepository.updateLastReadTime(user, LocalDateTime.now()); + + Slice alarmSlices = alarmRepository.findByUserAndIdLessThanOrderByIdDesc(user, cursor, PageRequest.of(0, take)); + + List alarmResponseDTOList = alarmSlices.stream().map(AlarmResponseDTO::toAlarmResponseDTO + ).toList(); + + Long nextCursor = null; + if (!alarmSlices.isLast()) { + nextCursor = alarmSlices.toList().get(alarmSlices.toList().size() - 1).getId(); + } + + return AlarmResponseListDTO.toAlarmResponseListDTO(alarmResponseDTOList, nextCursor, alarmSlices.hasNext()); + } + + @Override + public List findUnReadUserListByPost(User user, Long moimId, Long postId) { + Moim moim = moimRepository.findById(moimId).orElseThrow(() -> new MoimException(MOIM_NOT_FOUND)); + Post post = postRepository.findById(postId).orElseThrow(() -> new PostException(POST_NOT_FOUND)); + + if (!post.getPostType().equals(PostType.ANNOUNCEMENT)) { + throw new PostException(ErrorStatus.NOT_ANNOUNCEMENT_POST); + } + + Set readUsersId = userRepository.findReadUserId(post); + + List readUsers = userRepository.findReadUsersProfileByUsersId(readUsersId, moim); + + List userPreviewDTOList = readUsers.stream().map(UserPreviewDTO::toUserPreviewDTO).toList(); + + return userPreviewDTOList; + } + + @Override + public boolean existsByProviderAndProviderId(Provider provider, String providerId) { + return userRepository.existsByProviderAndProviderId(provider, providerId); + } + + @Override + public boolean existsByEmail(String email) { + return userRepository.existsByEmail(email); + } + + @Override + public Integer countAlarm(User user) { + List alarmByUser = userRepository.findAlarmByUser(user); + return alarmByUser.size(); + } + + @Override + public Optional findUserProfile(Long profileId) { + return userProfileRepository.findById(profileId); + } + + @Override + public boolean existsByUserProfileIdAndJoinStatus(Long profileId) { + return userMoimRepository.existsByUserProfileIdAndJoinStatus(profileId, JoinStatus.COMPLETE); + } +} + +// @Override +// public ChatRoomUserListResponse getUserByChatRoom(User user, Long chatRoomId) { +// if (!chatRoomRepository.existsChatRoomById(chatRoomId)) { +// throw new ChatRoomException(ErrorStatus.CHATROOM_NOT_FOUND); +// } +// +// ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow(() -> new ChatRoomException(ErrorStatus.CHATROOM_NOT_FOUND)); +// +// Optional byUserAndMoim = userMoimRepository.findByUserAndMoim(user, chatRoom.getMoim()); +// +// +// +// return ChatRoomUserListResponse(userByChatRoomId); +// } diff --git a/src/main/java/com/dev/moim/global/common/ApiResponse.java b/src/main/java/com/dev/moim/global/common/BaseResponse.java similarity index 58% rename from src/main/java/com/dev/moim/global/common/ApiResponse.java rename to src/main/java/com/dev/moim/global/common/BaseResponse.java index 1dd0c55d..0cc16793 100644 --- a/src/main/java/com/dev/moim/global/common/ApiResponse.java +++ b/src/main/java/com/dev/moim/global/common/BaseResponse.java @@ -11,7 +11,7 @@ @Getter @AllArgsConstructor @JsonPropertyOrder({"isSuccess", "code", "message", "result"}) -public class ApiResponse { +public class BaseResponse { @JsonProperty("isSuccess") private final Boolean isSuccess; @@ -23,17 +23,17 @@ public class ApiResponse { // 성공한 경우 응답 생성 - public static ApiResponse onSuccess(T result){ - return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result); + public static BaseResponse onSuccess(T result){ + return new BaseResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result); } - public static ApiResponse of(BaseCode code, T result){ - return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result); + public static BaseResponse of(BaseCode code, T result){ + return new BaseResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result); } // 실패한 경우 응답 생성 - public static ApiResponse onFailure(String code, String message, T data){ - return new ApiResponse<>(false, code, message, data); + public static BaseResponse onFailure(String code, String message, T data){ + return new BaseResponse<>(false, code, message, data); } } diff --git a/src/main/java/com/dev/moim/global/common/RootController.java b/src/main/java/com/dev/moim/global/common/RootController.java new file mode 100644 index 00000000..c5d9c5f3 --- /dev/null +++ b/src/main/java/com/dev/moim/global/common/RootController.java @@ -0,0 +1,13 @@ +package com.dev.moim.global.common; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class RootController { + + @GetMapping("/health") + public String healthCheck() { + return "I'm healthy!"; + } +} diff --git a/src/main/java/com/dev/moim/global/common/code/status/ErrorStatus.java b/src/main/java/com/dev/moim/global/common/code/status/ErrorStatus.java index 0d685bbd..48ae6a21 100644 --- a/src/main/java/com/dev/moim/global/common/code/status/ErrorStatus.java +++ b/src/main/java/com/dev/moim/global/common/code/status/ErrorStatus.java @@ -1,6 +1,5 @@ package com.dev.moim.global.common.code.status; - import com.dev.moim.global.common.code.BaseErrorCode; import com.dev.moim.global.common.code.ErrorReasonDTO; import lombok.AllArgsConstructor; @@ -17,7 +16,13 @@ public enum ErrorStatus implements BaseErrorCode { _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), // 공통 에러 - PAGE_UNDER_ZERO(HttpStatus.BAD_REQUEST, "COMM_001", "페이지는 0이상이어야 합니다."), + PAGE_UNDER_ZERO(HttpStatus.BAD_REQUEST, "COMMON_001", "페이지는 0이상이어야 합니다."), + MULTIPLE_FIELD_VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "COMMON_002", "입력된 정보에 오류가 있습니다. 필드별 오류 메시지를 참조하세요."), + NO_MATCHING_ERROR_STATUS(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_003", "서버 에러. 일치하는 errorStatus를 찾을 수 없습니다."), + REQUEST_BODY_INVALID(HttpStatus.BAD_REQUEST, "COMMON_004", "요청 본문을 읽을 수 없습니다. 빈 문자열 또는 null이 있는지 확인해주세요."), + + // Redis 관련 에러 + REDIS_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "REDIS_001", "Redis 연결에 실패했습니다. 관리자에게 문의 바랍니다."), // S3 관련 S3_OBJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "S3_001", "S3 오브젝트를 찾을 수 없습니다."), @@ -26,29 +31,124 @@ public enum ErrorStatus implements BaseErrorCode { // Auth 관련 AUTH_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_001", "토큰이 만료되었습니다."), AUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_002", "토큰이 유효하지 않습니다."), - INVALID_LOGIN_REQUEST(HttpStatus.UNAUTHORIZED, "AUTH_003", "올바른 이메일이나 패스워드가 아닙니다."), - INVALID_REQUEST_INFO(HttpStatus.UNAUTHORIZED, "AUTH_006", "카카오 정보 불러오기에 실패하였습니다."), + OAUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_003", "OAuth 토큰이 유효하지 않습니다."), + INVALID_LOGIN_REQUEST(HttpStatus.UNAUTHORIZED, "AUTH_004", "올바른 이메일이나 패스워드가 아닙니다."), + INVALID_REQUEST_INFO(HttpStatus.UNAUTHORIZED, "AUTH_005", "카카오 정보 불러오기에 실패하였습니다."), + USER_PROPERTY_NOT_FOUND(HttpStatus.BAD_REQUEST,"AUTH_006", "카카오 앱에 설정되어 있지 않은 사용자 프로퍼티를 요청하였습니다." ), NOT_EQUAL_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_007", "리프레시 토큰이 다릅니다."), NOT_CONTAIN_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_008", "해당하는 토큰이 저장되어있지 않습니다."), + BAD_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH_009", "비밀번호를 잘못 입력했습니다."), + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "AUTH_010", "인증에 실패했습니다."), + EMAIL_DUPLICATION(HttpStatus.BAD_REQUEST, "AUTH_011", "이미 가입한 메일 입니다."), + INVALID_GENDER(HttpStatus.BAD_REQUEST, "AUTH_012", "올바르지 않은 gender 입력값 입니다."), + USER_AUTHENTICATION_FAIL(HttpStatus.UNAUTHORIZED, "AUTH_013", "유저 인증에 실패했습니다."), + USER_INSUFFICIENT_PERMISSION(HttpStatus.FORBIDDEN, "AUTH_014", "권한이 부족한 사용자 입니다."), + MISSING_AUTHORIZATION_HEADER(HttpStatus.UNAUTHORIZED, "AUTH_015", "Authorization 헤더가 비어있습니다."), + PROVIDER_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH_016", "지원하지 않는 로그인 provider 입니다."), + ID_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "AUTH_017", "ID 토큰이 만료되었습니다."), + ID_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "AUTH_018", "유효하지 않은 ID 토큰 입니다."), + LOGOUT_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_019", "로그아웃된 access 토큰 입니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH_020", "존재하지 않는 사용자입니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "AUTH_021", "비밀번호 조건에 맞지 않습니다."), + OAUTH_ACCOUNT_DUPLICATION(HttpStatus.BAD_REQUEST, "AUTH_022", "이미 가입한 소셜 계정입니다."), + OAUTH_SECRET_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "AUTH_023", "OAuth 관련 환경 변수가 누락되었습니다."), + GET_OAUTH_USER_INFO_FAIL(HttpStatus.BAD_REQUEST, "AUTH_024", "OAuth 유저 정보 요청에 실패했습니다."), + PROVIDER_ID_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTH_025", "providerId가 누락되었습니다."), + INVALID_REQUEST_BODY(HttpStatus.BAD_REQUEST, "AUTH_026", "잘못된 요청 본문입니다."), + INVALID_REQUEST_HEADER(HttpStatus.BAD_REQUEST, "AUTH_027", "잘못된 요청 헤더입니다."), + USER_UNREGISTERED(HttpStatus.UNAUTHORIZED, "AUTH_028", "존재하지 않는 계정입니다. 회원가입을 진행해주세요."), + OIDC_PUBLIC_KEY_NOT_FOUND(HttpStatus.UNAUTHORIZED, "AUTH_029", "OIDC ID 토큰 공개키를 받아오는데 실패했습니다."), + HTTP_REQUEST_NULL(HttpStatus.BAD_REQUEST, "AUTH_030", "HttpServletRequest가 null입니다."), + + // Plan 관련 + INDIVIDUAL_PLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_001", "존재하지 않는 개인 일정 입니다."), + NOT_INDIVIDUAL_PLAN_OWNER(HttpStatus.UNAUTHORIZED, "PLAN_002", "해당 일정의 작성자가 아닙니다."), + ALREADY_PARTICIPATE_IN_PLAN(HttpStatus.BAD_REQUEST, "PLAN_003", "이미 해당 모임 일정에 참여 신청했습니다."), + USER_NOT_PART_OF_PLAN(HttpStatus.BAD_REQUEST, "PLAN_004", "해당 일정에 참여 신청하지 않았습니다."), + PLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAN_005", "존재하지 않는 일정입니다."), + PLAN_EDIT_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "PLAN_006", "모임 일정 수정, 삭제 권한이 없는 유저입니다." ), + + // Email 관련 + EMAIL_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "EMAIL_001", "이메일 전송에 실패했습니다."), + INCORRECT_EMAIL_CODE(HttpStatus.UNAUTHORIZED, "EMAIL_002", "이메일 인증 코드가 일치하지 않습니다."), + EMAIL_CODE_NOT_FOUND(HttpStatus.BAD_REQUEST, "EMAIL_003", "유저 이메일에 해당하는 이메일 코드가 저장되어있지 않습니다. 재요청을 시도해주세요."), + EMAIL_AUTHENTICATION_FAIL(HttpStatus.UNAUTHORIZED, "EMAIL_004", "이메일 인증에 실패했습니다."), + + // Moim 관련 + MOIM_NOT_FOUND(HttpStatus.NOT_FOUND, "MOIM_001", "모임을 찾을 수 없습니다."), + MOIM_NOT_ADMIN(HttpStatus.UNAUTHORIZED, "MOIM_002", "모임 관리자 회원이 아닙니다."), + INVALID_MOIM_MEMBER(HttpStatus.FORBIDDEN, "MOIM_003", "모임의 멤버가 아닙니다."), + IS_MOIM_OWNER(HttpStatus.BAD_REQUEST, "MOIM_004", "모임장 권한이 있는 유저입니다. 권한을 위임해주세요."), + PLAN_WRITER_NOT_FOUND(HttpStatus.NOT_FOUND, "MOIM_005", "해당 일정의 작성자를 찾을 수 없습니다."), + USER_NOT_MOIM_JOIN(HttpStatus.UNAUTHORIZED, "MOIM_006", "모임의 회원이 아닙니다."), + USER_NOT_MOIM_ADMIN(HttpStatus.FORBIDDEN, "MOIM_007", "모임의 관리자가 아닙니다."), + VIDEO_ERROR(HttpStatus.NOT_FOUND, "MOIM_008", "해당 모임의 관리자가 없거나 관리자의 프로필이 존재하지 않습니다."), + MOIM_OWNER_NOT_FOUND(HttpStatus.NOT_FOUND, "MOIM_009", "모임장 회원을 찾을 수 없습니다."), + ALREADY_REQUEST(HttpStatus.FORBIDDEN, "MOIM_010", "이미 신청한 모임 입니다.."), + NOT_REQUEST_JOIN(HttpStatus.NOT_FOUND, "MOIM_011", "신청하지 않은 모임입니다."), + USER_MOIM_NOT_FOUND(HttpStatus.NOT_FOUND, "MOIM_012", "user moim을 찾을 수 없습니다."), + OWNER_NOT_EXIT(HttpStatus.NOT_FOUND, "MOIM_013", "owner는 모임을 나갈 수 없습니다."), - // User 관련 - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH_004", "존재하지 않는 사용자입니다."), + // UserProfile 관련 + USER_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "USERPROFILE_001", "프로필을 찾을 수 없습니다."), + USER_PROFILE_NOT_FOUND_MAIN(HttpStatus.NOT_FOUND, "USERPROFILE_002", "메인 프로필을 찾을 수 없습니다."), + NOT_USER_PROFILE_OWNER(HttpStatus.UNAUTHORIZED, "USERPROFILE_003", "해당 유저의 프로필이 아닙니다."), + USER_PROFILE_IN_USE(HttpStatus.FORBIDDEN, "USERPROFILE_004", "해당 프로필을 사용 중인 모임이 있습니다."), + CANNOT_DELETE_MAIN_USER_PROFILE(HttpStatus.FORBIDDEN, "USERPROFILE_005", "대표 프로필은 삭제할 수 없습니다. 대표 프로필을 변경해주세요."), - // Recreation 관련 - SEARCH_CONDITION_INVALID(HttpStatus.BAD_REQUEST, "RECR_001", "검색 조건이 하나라도 존재해야 합니다."), - RECREATION_NOT_FOUND(HttpStatus.NOT_FOUND, "RECR_002", "존재하지 않는 레크레이션입니다."), + // page 관련 + NOT_VALID_CURSOR(HttpStatus.BAD_REQUEST, "PAGE_001", "커서 값이 유효하지 않습니다."), + NOT_VALID_TAKE(HttpStatus.BAD_REQUEST, "PAGE_002", "take 값이 유효하지 않습니다."), + NOT_VALID_PAGE(HttpStatus.BAD_REQUEST, "PAGE_003", "page 값이 유효하지 않습니다."), + NOT_VALID_SIZE(HttpStatus.BAD_REQUEST, "PAGE_004", "size 값이 유효하지 않습니다."), - // RecreationReview 관련 - REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "REV_001", "존재하지 않는 리뷰입니다."), + // Post + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_001", "POST를 찾을 수 없습니다."), + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_002", "COMMENT를 찾을 수 없습니다."), + BLOCK_POST(HttpStatus.FORBIDDEN, "POST_003", "해당 게시물이 차단 되어있습니다."), + NOT_MY_POST(HttpStatus.FORBIDDEN, "POST_004", "해당 작성물이 자신의 것이 아닙니다."), + ALREADY_COMMENT_DELETE(HttpStatus.FORBIDDEN, "POST_005", "이미 삭제된 댓글입니다."), + NOT_INCLUDE_POST(HttpStatus.FORBIDDEN, "POST_006", "해당 댓글이 게시물에 포함되어 있지 않습니다."), + NOT_ANNOUNCEMENT_POST(HttpStatus.FORBIDDEN, "POST_007", "해당 게시물은 공지사항이 아닙니다."), - // Flow 관련 - FLOW_NOT_FOUND(HttpStatus.NOT_FOUND, "FLO_001", "존재하지 않는 플로우입니다."), - FLOW_DELETE_UNAUTHORIZED(HttpStatus.FORBIDDEN, "FLOW_002", "삭제 권한이 없습니다."), + // 채팅 관련 에러 + INVALID_CHAT_SCROLL(HttpStatus.BAD_REQUEST, "CHAT4001", "더 이상 채팅이 존재하지 않습니다."), + CHAT_NOT_SEND(HttpStatus.BAD_REQUEST, "CHAT4002", "전송 중 오류가 발생하였습니다."), + INVALID_CHAT_FORMAT(HttpStatus.BAD_REQUEST, "CHAT4003", "채팅 형식이 유효하지 않습니다."), + + // 채팅방 관련 에러 + INVALID_CHATROOM_SCROLL(HttpStatus.BAD_REQUEST, "CHATROOM4001", "더 이상 채팅방이 존재하지 않습니다."), + CHATROOM_NOT_FOUND(HttpStatus.BAD_REQUEST, "CHATROOM4002", "해당 채팅방이 존재하지 않습니다."), + ALREADY_JOIN_CHATROOM(HttpStatus.BAD_REQUEST, "CHATROOM4003", "이미 채팅방에 들어와있습니다."), + FIRST_JOIN_CHATROOM(HttpStatus.BAD_REQUEST, "CHATROOM4004", "채팅방에 먼저 들어와 있어야 합니다."), + NOT_JOIN_CHATROOM(HttpStatus.BAD_REQUEST, "CHATROOM4005", "채팅방에 가입되있지 않습니다."), + FAILED_ENTER_CHATROOM(HttpStatus.INTERNAL_SERVER_ERROR, "CHATROOM5001", "채팅 방 들어가기에 실패하였습니다"), + FAILED_EXIT_CHATROOM(HttpStatus.INTERNAL_SERVER_ERROR, "CHATROOM5002", "채팅 방 나가기에 실패하였습니다"), + INVALID_CHATROOM(HttpStatus.INTERNAL_SERVER_ERROR, "CHATROOM5003", "정상적인 채팅방이 아닙니다. 새로운 채팅방을 다시 생성해주세요"), + OVERLAP_JOIN_USER(HttpStatus.BAD_REQUEST, "CHATROOM4006", "들어오는 user와 participant의 user의 id가 같으면 안됩니다."), // FeignClient 관련 FEIGN_400_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FEI_001", "FeignClient 400번대 에러 발생"), FEIGN_500_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FEI_002", "FeignClient 500번대 에러 발생"), FEIGN_UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FEI_003", "FeignClient 알 수 없는 에러 발생"), + + // FCM 관련 에러 + FCM_NOT_VALID(HttpStatus.UNAUTHORIZED, "FCM_001", "FCM 토큰이 유효하지 않습니다.."), + FCM_TOKEN_REQUIRED(HttpStatus.BAD_REQUEST, "FCM_002", "FCM 토큰이 누락되었습니다."), + + // Review 관련 + SELF_REVIEW_FORBIDDEN(HttpStatus.FORBIDDEN, "REVIEW_001", "유저 본인에게 리뷰를 남길 수 없습니다."), + + // Todo 관련 + TODO_NOT_FOUND(HttpStatus.NOT_FOUND, "TODO_001", "Todo를 찾을 수 없습니다."), + NOT_TODO_ASSIGNEE(HttpStatus.UNAUTHORIZED, "TODO_002", "해당 유저에게 부여된 todo가 아닙니다."), + TODO_STATUS_SAME(HttpStatus.BAD_REQUEST, "TODO_003", "업데이트 요청한 todo status가 기존 status와 동일합니다."), + TODO_ASSIGNEE_NULL(HttpStatus.BAD_REQUEST, "TODO_004", "Todo를 할당받을 유저를 지정하지 않았습니다."), + TODO_ASSIGNEE_NOT_MATCH(HttpStatus.BAD_REQUEST, "TODO_005", "전체 선택인 경우 특정 assignee를 지정할 수 없습니다."), + TODO_DUE_DATE_EXPIRED(HttpStatus.BAD_REQUEST, "TODO_006", "마감 기한이 지난 Todo입니다."), + TODO_INVALID_STATE_REQUEST(HttpStatus.BAD_REQUEST, "TODO_007", "해당 todo status로 변경할 수 없습니다."), + IS_ALREADY_TODO_ASSIGNEE(HttpStatus.BAD_REQUEST, "TODO_008", "이미 todo를 할당받은 멤버를 지정했습니다."), + INVALID_TODO_DUE_DATE(HttpStatus.BAD_REQUEST, "TODO_009", "todo 마감 기한을 현재 날짜 이전으로 수정할 수 없습니다.") ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/dev/moim/global/common/code/status/SuccessStatus.java b/src/main/java/com/dev/moim/global/common/code/status/SuccessStatus.java index bff0f57f..0886156b 100644 --- a/src/main/java/com/dev/moim/global/common/code/status/SuccessStatus.java +++ b/src/main/java/com/dev/moim/global/common/code/status/SuccessStatus.java @@ -11,7 +11,10 @@ @AllArgsConstructor public enum SuccessStatus implements BaseCode { _OK(HttpStatus.OK, "COMMON200", "성공입니다."), - _CREATED(HttpStatus.CREATED, "COMMON201", "요청 성공 및 리소스 생성됨"); + _CREATED(HttpStatus.CREATED, "COMMON201", "요청 성공 및 리소스 생성됨"), + + // 소셜 로그인 + UNREGISTERED_OAUTH_LOGIN_USER(HttpStatus.ACCEPTED, "AUTH_001", "신규 유저 입니다. 회원가입을 진행해주세요."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/dev/moim/global/config/CorsConfig.java b/src/main/java/com/dev/moim/global/config/CorsConfig.java new file mode 100644 index 00000000..deda00bd --- /dev/null +++ b/src/main/java/com/dev/moim/global/config/CorsConfig.java @@ -0,0 +1,35 @@ +package com.dev.moim.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; +import java.util.List; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Bean + public static CorsConfigurationSource apiConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + List allowedOriginPatterns = Arrays.asList("*"); + configuration.setAllowedOriginPatterns(allowedOriginPatterns); + + List allowedHttpMethods = Arrays.asList("GET", "POST", "PUT", "DELETE"); + configuration.setAllowedMethods(allowedHttpMethods); + + List allowedHeaders = Arrays.asList("*"); + configuration.setAllowedHeaders(allowedHeaders); + + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/com/dev/moim/global/config/QuerydslConfig.java b/src/main/java/com/dev/moim/global/config/QuerydslConfig.java new file mode 100644 index 00000000..55a3ee53 --- /dev/null +++ b/src/main/java/com/dev/moim/global/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package com.dev.moim.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@RequiredArgsConstructor +@Configuration +public class QuerydslConfig { + + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/dev/moim/global/config/SwaggerConfig.java b/src/main/java/com/dev/moim/global/config/SwaggerConfig.java index 9bef3354..161eebd3 100644 --- a/src/main/java/com/dev/moim/global/config/SwaggerConfig.java +++ b/src/main/java/com/dev/moim/global/config/SwaggerConfig.java @@ -9,28 +9,26 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import static io.swagger.v3.oas.models.security.SecurityScheme.*; - @Configuration public class SwaggerConfig { - @Bean - public OpenAPI moimAPI() { - Info info = new Info().title("Moim API").description("Moim API 명세").version("0.0.1"); + public OpenAPI UMCstudyAPI() { + Info info = new Info() + .title("Moim server API") + .description("Moim Server API 명세서") + .version("1.0.0"); String jwtSchemeName = "JWT TOKEN"; + // API 요청헤더에 인증정보 포함 SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName); - - Components components = - new Components() - .addSecuritySchemes( - jwtSchemeName, - new SecurityScheme() - .name(jwtSchemeName) - .type(Type.HTTP) - .scheme("Bearer") - .bearerFormat("JWT")); + // SecuritySchemes 등록 + Components components = new Components() + .addSecuritySchemes(jwtSchemeName, new SecurityScheme() + .name(jwtSchemeName) + .type(SecurityScheme.Type.HTTP) // HTTP 방식 + .scheme("bearer") + .bearerFormat("JWT")); return new OpenAPI() .addServersItem(new Server().url("/")) diff --git a/src/main/java/com/dev/moim/global/config/WebConfig.java b/src/main/java/com/dev/moim/global/config/WebConfig.java new file mode 100644 index 00000000..2dc04f84 --- /dev/null +++ b/src/main/java/com/dev/moim/global/config/WebConfig.java @@ -0,0 +1,34 @@ +package com.dev.moim.global.config; + +import com.dev.moim.global.security.annotation.AuthUserArgumentResolver; +import com.dev.moim.global.security.annotation.ExtractTokenArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final AuthUserArgumentResolver authUserArgumentResolver; + private final ExtractTokenArgumentResolver extractTokenArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authUserArgumentResolver); + resolvers.add(extractTokenArgumentResolver); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(false) + .maxAge(6000); + } +} diff --git a/src/main/java/com/dev/moim/global/email/EmailConfig.java b/src/main/java/com/dev/moim/global/email/EmailConfig.java new file mode 100644 index 00000000..7a2670e8 --- /dev/null +++ b/src/main/java/com/dev/moim/global/email/EmailConfig.java @@ -0,0 +1,67 @@ +package com.dev.moim.global.email; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class EmailConfig { + @Value("${spring.mail.host}") + private String host; + + @Value("${spring.mail.port}") + private int port; + + @Value("${spring.mail.username}") + private String username; + + @Value("${spring.mail.password}") + private String password; + + @Value("${spring.mail.properties.mail.smtp.auth}") + private boolean auth; + + @Value("${spring.mail.properties.mail.smtp.starttls.enable}") + private boolean starttlsEnable; + + @Value("${spring.mail.properties.mail.smtp.starttls.required}") + private boolean starttlsRequired; + + @Value("${spring.mail.properties.mail.smtp.connectiontimeout}") + private int connectionTimeout; + + @Value("${spring.mail.properties.mail.smtp.timeout}") + private int timeout; + + @Value("${spring.mail.properties.mail.smtp.writetimeout}") + private int writeTimeout; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + mailSender.setDefaultEncoding("UTF-8"); + mailSender.setJavaMailProperties(getMailProperties()); + + return mailSender; + } + + private Properties getMailProperties() { + Properties properties = new Properties(); + properties.put("mail.smtp.auth", auth); + properties.put("mail.smtp.starttls.enable", starttlsEnable); + properties.put("mail.smtp.starttls.required", starttlsRequired); + properties.put("mail.smtp.connectiontimeout", connectionTimeout); + properties.put("mail.smtp.timeout", timeout); + properties.put("mail.smtp.writetimeout", writeTimeout); + + return properties; + } +} diff --git a/src/main/java/com/dev/moim/global/email/EmailUtil.java b/src/main/java/com/dev/moim/global/email/EmailUtil.java new file mode 100644 index 00000000..6f29a4f5 --- /dev/null +++ b/src/main/java/com/dev/moim/global/email/EmailUtil.java @@ -0,0 +1,173 @@ +package com.dev.moim.global.email; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.global.error.handler.EmailException; +import com.dev.moim.global.redis.util.RedisUtil; +import jakarta.mail.Message; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import java.util.Random; + +import static com.dev.moim.global.common.code.status.ErrorStatus.EMAIL_SEND_FAIL; + +@Component +@RequiredArgsConstructor +@Slf4j +public class EmailUtil { + + private final JavaMailSender emailSender; + private final RedisUtil redisUtil; + @Value("${spring.mail.auth-code-expiration-millis}") + private long authCodeExpirationMillis; + @Value("${app.s3.logo-url}") + private String logoUrl; + + public String sendAuthorizationCodeEmail(String receiver) throws Exception { + String code = createCode(); + MimeMessage message = createAuthorizationCodeMessage(receiver, code); + + try { + emailSender.send(message); + } catch (MailException e) { + throw new EmailException(EMAIL_SEND_FAIL); + } + emailSender.send(message); + redisUtil.setValue(receiver, code, this.authCodeExpirationMillis); + + return code; + } + + private String createCode() { + StringBuffer code = new StringBuffer(); + Random random = new Random(); + + for (int i = 0; i < 8; i++) { + int index = random.nextInt(3); + + switch (index) { + case 0: + code.append((char) (random.nextInt(26) + 'A')); + break; + case 1: + code.append((char) (random.nextInt(26) + 'a')); + break; + case 2: + code.append((random.nextInt(10))); + break; + } + } + return code.toString(); + } + + private MimeMessage createAuthorizationCodeMessage(String receiver, String code) throws Exception { + MimeMessage message = emailSender.createMimeMessage(); + InternetAddress[] recipients = {new InternetAddress(receiver)}; + message.setSubject("MOIM 회원가입 인증 코드"); + message.setRecipients(Message.RecipientType.TO, recipients); + + String msg = "" + + "" + + "" + + " " + + " " + + " MOIM 이메일 인증 코드 입니다" + + "" + + "" + + "
" + + "
" + + "

MOIM

" + + "
" + + "
" + + "

안녕하세요!

" + + "

MOIM에 가입해 주셔서 감사합니다. 아래 인증 코드를 입력하여 회원가입을 완료해 주세요.

" + + "
" + + "
" + + "
" + code + "
" + + "
" + + "
" + + "

인증 코드는 발송 시점부터 10분간 유효합니다.

" + + "
" + + "

감사합니다,
MOIM 팀

" + + "
" + + "
" + + "

이 메일은 MOIM 서비스에서 자동으로 발송되었습니다. 회신하지 마세요.

" + + "

Copyright ⓒ MOIM. All Rights Reserved.

" + + "
" + + "
" + + "" + + ""; + + message.setContent(msg, "text/html; charset=utf-8"); + message.setFrom(new InternetAddress("${spring.mail.username}", "MOIM")); + + return message; + } + + public void sendInquiryEmail(User user, String receiver, String content) throws Exception { + MimeMessage message = createInquiryMessage(user, receiver, content); + + try { + emailSender.send(message); + } catch (MailException e) { + throw new EmailException(EMAIL_SEND_FAIL); + } + } + + private MimeMessage createInquiryMessage(User user, String userEmail, String inquiryContent) throws Exception { + MimeMessage message = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setSubject("MOIM 유저 의견 및 문의사항"); + helper.setTo("moim2moim@gmail.com"); + helper.setFrom(new InternetAddress("moim2moim@gmail.com", "MOIM")); + + String msg = "" + + "" + + "" + + " " + + " " + + " 문의 메일" + + "" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "
" + + " MOIM 로고" + + "
MOIM
" + + "
" + + "
의견 및 문의사항
" + + "
" + + "

회신용 이메일

" + + "
" + userEmail + "
" + + "

문의 내용

" + + "
" + inquiryContent + "
" + + "
" + + "

Copyright ⓒ MOIM. All Rights Reserved.

" + + "
" + + "" + + ""; + + helper.setText(msg, true); + + return message; + } +} diff --git a/src/main/java/com/dev/moim/global/error/ExceptionAdvice.java b/src/main/java/com/dev/moim/global/error/ExceptionAdvice.java index 75948700..e31540a3 100644 --- a/src/main/java/com/dev/moim/global/error/ExceptionAdvice.java +++ b/src/main/java/com/dev/moim/global/error/ExceptionAdvice.java @@ -1,15 +1,21 @@ package com.dev.moim.global.error; -import com.dev.moim.global.common.ApiResponse; +import com.dev.moim.global.common.BaseResponse; import com.dev.moim.global.common.code.ErrorReasonDTO; import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.feign.dto.DiscordMessage; +import com.dev.moim.global.error.feign.service.DiscordClient; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestController; @@ -17,60 +23,107 @@ import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import static com.dev.moim.global.common.code.status.ErrorStatus.NO_MATCHING_ERROR_STATUS; +import static com.dev.moim.global.common.code.status.ErrorStatus.REQUEST_BODY_INVALID; + @Slf4j @RestControllerAdvice(annotations = {RestController.class}) +@RequiredArgsConstructor public class ExceptionAdvice extends ResponseEntityExceptionHandler { + private final DiscordClient discordClient; + private final Environment environment; - @org.springframework.web.bind.annotation.ExceptionHandler + @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = e.getConstraintViolations().stream() - .map(constraintViolation -> constraintViolation.getMessage()) + .map(ConstraintViolation::getMessage) .findFirst() .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); - return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request); + ErrorStatus errorStatus = mapToErrorStatus(errorMessage); + + if (errorStatus == null) { + throw new IllegalArgumentException(String.valueOf(NO_MATCHING_ERROR_STATUS)); + } + + return handleExceptionInternalConstraint(e, errorStatus, HttpHeaders.EMPTY,request); } @Override - public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + Map errors = new LinkedHashMap<>(); - ex.getBindingResult().getFieldErrors().stream() - .forEach(fieldError -> { - String fieldName = fieldError.getField(); - String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); - errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); - }); + ex.getBindingResult().getFieldErrors().forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + ErrorStatus errorStatus = mapToErrorStatus(errors.values().iterator().next()); - return handleExceptionInternalArgs(ex,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors); + if (errors.size() != 1 || errorStatus == null) { + errorStatus = ErrorStatus.MULTIPLE_FIELD_VALIDATION_ERROR; + } + return handleExceptionInternalArgs(ex, HttpHeaders.EMPTY, errorStatus, request, errors); } - @org.springframework.web.bind.annotation.ExceptionHandler + @ExceptionHandler public ResponseEntity exception(Exception e, WebRequest request) { - e.printStackTrace(); - return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage()); + if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) { + sendDiscordAlarm(e, request); + } + + return handleExceptionInternalFalse( + e, + ErrorStatus._INTERNAL_SERVER_ERROR, + HttpHeaders.EMPTY, + ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), + request, + e.getMessage()); } @ExceptionHandler(value = GeneralException.class) - public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { + public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { + ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); + return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request); } + @Override + protected ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + + BaseResponse body = BaseResponse.onFailure(ErrorStatus._BAD_REQUEST.getCode(), REQUEST_BODY_INVALID.getMessage(), null); + + return handleExceptionInternal(e, body, HttpHeaders.EMPTY, ErrorStatus._BAD_REQUEST.getHttpStatus(), request); + } + private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) { - ApiResponse body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null); -// e.printStackTrace(); + BaseResponse body = BaseResponse.onFailure(reason.getCode(),reason.getMessage(),null); WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal( e, body, @@ -82,7 +135,9 @@ private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonD private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { - ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint); + + BaseResponse body = BaseResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint); + return super.handleExceptionInternal( e, body, @@ -94,7 +149,9 @@ private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorSt private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus, WebRequest request, Map errorArgs) { - ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs); + + BaseResponse body = BaseResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs); + return super.handleExceptionInternal( e, body, @@ -106,7 +163,9 @@ private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHead private ResponseEntity handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, WebRequest request) { - ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + + BaseResponse body = BaseResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + return super.handleExceptionInternal( e, body, @@ -115,4 +174,57 @@ private ResponseEntity handleExceptionInternalConstraint(Exception e, Er request ); } + + private void sendDiscordAlarm(Exception e, WebRequest request) { + discordClient.sendAlarm(createMessage(e, request)); + } + + private DiscordMessage createMessage(Exception e, WebRequest request) { + return DiscordMessage.builder() + .content("# 🚨 에러 발생 비이이이이사아아아앙") + .embeds( + List.of( + DiscordMessage.Embed.builder() + .title("ℹ️ 에러 정보") + .description( + "### 🕖 발생 시간\n" + + LocalDateTime.now() + + "\n" + + "### 🔗 요청 URL\n" + + createRequestFullPath(request) + + "\n" + + "### 📄 Stack Trace\n" + + "```\n" + + getStackTrace(e).substring(0, 1000) + + "\n```") + .build())) + .build(); + } + + private String createRequestFullPath(WebRequest webRequest) { + HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); + String fullPath = request.getMethod() + " " + request.getRequestURL(); + + String queryString = request.getQueryString(); + if (queryString != null) { + fullPath += "?" + queryString; + } + + return fullPath; + } + + private String getStackTrace(Exception e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } + + private ErrorStatus mapToErrorStatus(String errorMessage) { + for (ErrorStatus status : ErrorStatus.values()) { + if (status.getMessage().equals(errorMessage)) { + return status; + } + } + return null; + } } diff --git a/src/main/java/com/dev/moim/global/error/feign/dto/DiscordMessage.java b/src/main/java/com/dev/moim/global/error/feign/dto/DiscordMessage.java new file mode 100644 index 00000000..255ec884 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/feign/dto/DiscordMessage.java @@ -0,0 +1,30 @@ +package com.dev.moim.global.error.feign.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class DiscordMessage { + + private String content; + private List embeds; + + @Builder + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @Getter + public static class Embed { + + private String title; + private String description; + } +} + diff --git a/src/main/java/com/dev/moim/global/error/feign/service/DiscordClient.java b/src/main/java/com/dev/moim/global/error/feign/service/DiscordClient.java new file mode 100644 index 00000000..a35b8d03 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/feign/service/DiscordClient.java @@ -0,0 +1,46 @@ +package com.dev.moim.global.error.feign.service; + +import com.dev.moim.global.error.feign.dto.DiscordMessage; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class DiscordClient { + + private final RestTemplate restTemplate = new RestTemplate(); + + private final ObjectMapper objectMapper = new ObjectMapper(); // Jackson ObjectMapper + + @Value("${spring.discord-url}") + private String WEBHOOK_URL; + + public void sendAlarm(DiscordMessage discordMessage) { + try { + + String jsonMessage = objectMapper.writeValueAsString(discordMessage); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + + HttpEntity request = new HttpEntity<>(jsonMessage, headers); + + ResponseEntity response = restTemplate.exchange(WEBHOOK_URL, HttpMethod.POST, request, String.class); + + if (response.getStatusCode() != HttpStatus.NO_CONTENT) { + throw new RuntimeException("Failed to send Discord message: " + response.getStatusCode()); + } + } catch (JsonProcessingException e) { + e.printStackTrace(); + throw new RuntimeException("Failed to send Discord message", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/error/handler/AuthException.java b/src/main/java/com/dev/moim/global/error/handler/AuthException.java new file mode 100644 index 00000000..d308e3fc --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/AuthException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class AuthException extends GeneralException { + public AuthException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/ChatException.java b/src/main/java/com/dev/moim/global/error/handler/ChatException.java new file mode 100644 index 00000000..96bc73d2 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/ChatException.java @@ -0,0 +1,11 @@ +package com.dev.moim.global.error.handler; + + +import com.dev.moim.global.common.code.BaseErrorCode; +import com.dev.moim.global.error.GeneralException; + +public class ChatException extends GeneralException { + public ChatException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/dev/moim/global/error/handler/ChatRoomException.java b/src/main/java/com/dev/moim/global/error/handler/ChatRoomException.java new file mode 100644 index 00000000..16980b0e --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/ChatRoomException.java @@ -0,0 +1,11 @@ +package com.dev.moim.global.error.handler; + + +import com.dev.moim.global.common.code.BaseErrorCode; +import com.dev.moim.global.error.GeneralException; + +public class ChatRoomException extends GeneralException { + public ChatRoomException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/dev/moim/global/error/handler/CommentException.java b/src/main/java/com/dev/moim/global/error/handler/CommentException.java new file mode 100644 index 00000000..5df73df2 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/CommentException.java @@ -0,0 +1,10 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.BaseErrorCode; +import com.dev.moim.global.error.GeneralException; + +public class CommentException extends GeneralException { + public CommentException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/dev/moim/global/error/handler/EmailException.java b/src/main/java/com/dev/moim/global/error/handler/EmailException.java new file mode 100644 index 00000000..e348a898 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/EmailException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class EmailException extends GeneralException { + public EmailException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/FeignException.java b/src/main/java/com/dev/moim/global/error/handler/FeignException.java new file mode 100644 index 00000000..809028c7 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/FeignException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class FeignException extends GeneralException { + public FeignException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/IndividualPlanException.java b/src/main/java/com/dev/moim/global/error/handler/IndividualPlanException.java new file mode 100644 index 00000000..a81c436e --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/IndividualPlanException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class IndividualPlanException extends GeneralException { + public IndividualPlanException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/MoimException.java b/src/main/java/com/dev/moim/global/error/handler/MoimException.java new file mode 100644 index 00000000..2c5b9e78 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/MoimException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class MoimException extends GeneralException { + public MoimException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/PlanException.java b/src/main/java/com/dev/moim/global/error/handler/PlanException.java new file mode 100644 index 00000000..2e8aaf11 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/PlanException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class PlanException extends GeneralException { + public PlanException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/PostException.java b/src/main/java/com/dev/moim/global/error/handler/PostException.java new file mode 100644 index 00000000..bbe0d332 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/PostException.java @@ -0,0 +1,10 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.BaseErrorCode; +import com.dev.moim.global.error.GeneralException; + +public class PostException extends GeneralException { + public PostException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/dev/moim/global/error/handler/TodoException.java b/src/main/java/com/dev/moim/global/error/handler/TodoException.java new file mode 100644 index 00000000..aef3222b --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/TodoException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class TodoException extends GeneralException { + public TodoException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/error/handler/UserException.java b/src/main/java/com/dev/moim/global/error/handler/UserException.java new file mode 100644 index 00000000..7a8952f6 --- /dev/null +++ b/src/main/java/com/dev/moim/global/error/handler/UserException.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.error.handler; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.GeneralException; + +public class UserException extends GeneralException { + public UserException(ErrorStatus errorStatus) {super(errorStatus);} +} diff --git a/src/main/java/com/dev/moim/global/firebase/FirebaseConfig.java b/src/main/java/com/dev/moim/global/firebase/FirebaseConfig.java new file mode 100644 index 00000000..eda47b37 --- /dev/null +++ b/src/main/java/com/dev/moim/global/firebase/FirebaseConfig.java @@ -0,0 +1,30 @@ +package com.dev.moim.global.firebase; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +@Configuration +public class FirebaseConfig { + + @Value("${spring.firebase.config}") + private String firebaseConfig; + + @Bean + public FirebaseApp firebaseApp() throws IOException { + ByteArrayInputStream serviceAccountStream = new ByteArrayInputStream(firebaseConfig.getBytes()); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccountStream)) + .build(); + + // FirebaseApp 인스턴스를 초기화하고 반환 + return FirebaseApp.initializeApp(options); + } +} diff --git a/src/main/java/com/dev/moim/global/firebase/service/FcmQueryService.java b/src/main/java/com/dev/moim/global/firebase/service/FcmQueryService.java new file mode 100644 index 00000000..a8c0be55 --- /dev/null +++ b/src/main/java/com/dev/moim/global/firebase/service/FcmQueryService.java @@ -0,0 +1,33 @@ +package com.dev.moim.global.firebase.service; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.AuthException; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class FcmQueryService { + + public void isTokenValid(String sender, String token) { + Notification notification = Notification.builder() + .setTitle(sender) + .setBody("토큰 인증 되었 습니다.") + .build(); + + Message message = Message.builder() + .setToken(token) + .setNotification(notification) + .build(); + + try { + FirebaseMessaging.getInstance().send(message); + } catch (FirebaseMessagingException e) { + throw new AuthException(ErrorStatus.FCM_NOT_VALID); + } + } +} diff --git a/src/main/java/com/dev/moim/global/firebase/service/FcmService.java b/src/main/java/com/dev/moim/global/firebase/service/FcmService.java new file mode 100644 index 00000000..9c17b7f0 --- /dev/null +++ b/src/main/java/com/dev/moim/global/firebase/service/FcmService.java @@ -0,0 +1,113 @@ +package com.dev.moim.global.firebase.service; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.user.dto.EventDTO; +import com.dev.moim.domain.user.service.UserCommandService; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.error.ExceptionAdvice; +import com.dev.moim.global.error.feign.dto.DiscordMessage; +import com.dev.moim.global.error.feign.service.DiscordClient; +import com.google.firebase.messaging.*; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FcmService { + + private final DiscordClient discordClient; + private final UserQueryService userQueryService; + private final AlarmService alarmService; + private final UserCommandService userCommandService; + private final Environment environment; + + public void sendEventAlarm(User owner, EventDTO eventDTO) { + List users = userQueryService.findAllUser(); + users.forEach(user -> { + if (user.getIsEventAlarm()) { + alarmService.saveAlarm(owner, user, eventDTO.title(), eventDTO.content(), AlarmType.EVENT, AlarmDetailType.EVENT, null, null, null); + sendPushNotification(user, eventDTO.title(), eventDTO.content(), AlarmDetailType.EVENT); + } + }); + } + + public void sendPushNotification(User receiver, String title, String body, AlarmDetailType alarmDetailType) { + + if (!(receiver.getDeviceId() == null)) { + + Integer count = userQueryService.countAlarm(receiver); + + Notification notification = Notification.builder() + .setTitle(title) + .setBody(body) + .build(); + + AndroidNotification androidNotification = AndroidNotification.builder() + .setChannelId(alarmDetailType.toString()) + .build(); + + AndroidConfig androidConfig = AndroidConfig.builder() + .setNotification(androidNotification) + .build(); + + ApnsFcmOptions apnsFcmOptions = ApnsFcmOptions.builder() + .setAnalyticsLabel(alarmDetailType.toString()) + .build(); + + ApnsConfig apnsConfig = ApnsConfig.builder() + .setAps(Aps.builder() + .setAlert(ApsAlert.builder() + .setTitle(title) + .setBody(body) + .build()) + .setContentAvailable(true) + .build()) + .setFcmOptions(apnsFcmOptions) + .build(); + + Message message = Message.builder() + .setNotification(notification) + .setToken(receiver.getDeviceId()) + .setAndroidConfig(androidConfig) + .setApnsConfig(apnsConfig) + .putData("count", count.toString()) + .build(); + + try { + FirebaseMessaging.getInstance().send(message); + } catch (FirebaseMessagingException e) { + userCommandService.notDeadLockFcmSignOut(receiver); + if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) { + discordClient.sendAlarm(createMessage(receiver, title, e)); + } + } + } + } + + private DiscordMessage createMessage(User receiver, String title, Exception e) { + return DiscordMessage.builder() + .content("# 🚨 에러 발생 비이이이이사아아아앙") + .embeds( + List.of( + DiscordMessage.Embed.builder() + .title("ℹ️ 에러 정보") + .description(String.format("%d가 유효하지 않은 fcm token값을 가지고 있습니다. \n 이유: %s\n", receiver.getId(), getStackTrace(e).substring(0, 1000))) + .build())) + .build(); + } + + private String getStackTrace(Exception e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } +} diff --git a/src/main/java/com/dev/moim/global/redis/config/RedisCacheConfig.java b/src/main/java/com/dev/moim/global/redis/config/RedisCacheConfig.java new file mode 100644 index 00000000..28a5003f --- /dev/null +++ b/src/main/java/com/dev/moim/global/redis/config/RedisCacheConfig.java @@ -0,0 +1,36 @@ +package com.dev.moim.global.redis.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + + @Bean + public CacheManager oidcCacheManager(RedisConnectionFactory cf) { + RedisCacheConfiguration redisCacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofDays(3L)); + + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf) + .cacheDefaults(redisCacheConfiguration) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/redis/config/RedisConfig.java b/src/main/java/com/dev/moim/global/redis/config/RedisConfig.java new file mode 100644 index 00000000..c8b90436 --- /dev/null +++ b/src/main/java/com/dev/moim/global/redis/config/RedisConfig.java @@ -0,0 +1,34 @@ +package com.dev.moim.global.redis.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public LettuceConnectionFactory connectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + StringRedisTemplate redisTemplate = new StringRedisTemplate(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(connectionFactory()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/dev/moim/global/redis/util/RedisUtil.java b/src/main/java/com/dev/moim/global/redis/util/RedisUtil.java new file mode 100644 index 00000000..6162efbe --- /dev/null +++ b/src/main/java/com/dev/moim/global/redis/util/RedisUtil.java @@ -0,0 +1,29 @@ +package com.dev.moim.global.redis.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +@Service +@RequiredArgsConstructor +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + public void setValue(String key, String value, Long expireInMillis) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, value, Duration.ofMillis(expireInMillis)); + } + + public String getValue(String key) { + ValueOperations values = redisTemplate.opsForValue(); + return values.get(key); + } + + public void deleteValue(String key) { + redisTemplate.delete(key); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/s3/S3Config.java b/src/main/java/com/dev/moim/global/s3/S3Config.java new file mode 100644 index 00000000..014a644d --- /dev/null +++ b/src/main/java/com/dev/moim/global/s3/S3Config.java @@ -0,0 +1,28 @@ +package com.dev.moim.global.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials= new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/s3/controller/S3Controller.java b/src/main/java/com/dev/moim/global/s3/controller/S3Controller.java new file mode 100644 index 00000000..9540c3fd --- /dev/null +++ b/src/main/java/com/dev/moim/global/s3/controller/S3Controller.java @@ -0,0 +1,33 @@ +package com.dev.moim.global.s3.controller; + + +import com.dev.moim.global.common.BaseResponse; + +import com.dev.moim.global.s3.dto.AwsDTO.*; +import com.dev.moim.global.s3.service.S3Service; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v0/s3") +public class S3Controller { + + private final S3Service s3Service; + + @Operation(summary = "Upload용 Presigned URL 생성", description = "업로드를 위한 Presigned URL을 생성한다") + @PostMapping("/presigned/upload") + public BaseResponse getPresignedUrlToUpload(@RequestBody PresignedUploadRequest presignedUploadRequest) { + PresignedUrlUploadResponse presignedUrlToUpload = s3Service.getPresignedUrlToUpload(presignedUploadRequest); + return BaseResponse.onSuccess(presignedUrlToUpload); + } + + @Operation(summary = "여러 개 Upload용 Presigned URL 생성", description = "업로드를 위한 Presigned URL를 여러 개 생성한다") + @PostMapping("/presigned/upload/list") + public BaseResponse getPresignedUrlToUploadList(@RequestBody PresignedUploadListRequest presignedUploadListRequest) { + PresignedUrlUploadResponseList presignedUrlToUploadList = s3Service.getPresignedUrlToUploadList(presignedUploadListRequest); + return BaseResponse.onSuccess(presignedUrlToUploadList); + } +} diff --git a/src/main/java/com/dev/moim/global/s3/dto/AwsDTO.java b/src/main/java/com/dev/moim/global/s3/dto/AwsDTO.java new file mode 100644 index 00000000..506957e5 --- /dev/null +++ b/src/main/java/com/dev/moim/global/s3/dto/AwsDTO.java @@ -0,0 +1,42 @@ +package com.dev.moim.global.s3.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class AwsDTO { + + @Schema(description = "AWS S3 URL 응답 정보 리스트") + @Builder + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor(access = AccessLevel.PROTECTED) + public static class PresignedUrlUploadResponseList { + private List presignedUrlUploadResponses; + } + + @Schema(description = "AWS S3 URL 응답 정보") + @Builder + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor(access = AccessLevel.PROTECTED) + public static class PresignedUrlUploadResponse { + private String url; + private String keyName; + } + + @Getter + public static class PresignedUploadListRequest { + private List fileNameList; + } + + @Getter + public static class PresignedUploadRequest { + private String fileName; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/s3/service/S3Service.java b/src/main/java/com/dev/moim/global/s3/service/S3Service.java new file mode 100644 index 00000000..de3dffe8 --- /dev/null +++ b/src/main/java/com/dev/moim/global/s3/service/S3Service.java @@ -0,0 +1,87 @@ +package com.dev.moim.global.s3.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.dev.moim.global.s3.dto.AwsDTO.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class S3Service { + + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public PresignedUrlUploadResponse getPresignedUrlToUpload(PresignedUploadRequest presignedUploadRequest) { + + /// 제한시간 설정 + Date expiration = new Date(); + long expTime = expiration.getTime(); + expTime += TimeUnit.MINUTES.toMillis(3); // 3 Minute + expiration.setTime(expTime); + + String keyName = UUID.randomUUID() + "_" + presignedUploadRequest.getFileName(); + + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, keyName) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + + String key = generatePresignedUrlRequest.getKey(); + + return PresignedUrlUploadResponse.builder() + .url(amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString()) + .keyName(key) + .build(); + } + + public Boolean isExistKeyName(String keyName) { + return amazonS3.doesObjectExist(bucket, keyName); + } + + public PresignedUrlUploadResponseList getPresignedUrlToUploadList(PresignedUploadListRequest presignedUploadListRequest) { + + /// 제한시간 설정 + Date expiration = new Date(); + long expTime = expiration.getTime(); + expTime += TimeUnit.MINUTES.toMillis(3); // 3 Minute + expiration.setTime(expTime); + + + + List responses = presignedUploadListRequest.getFileNameList().stream() + .map(oldKeyName -> { + String keyName = UUID.randomUUID() + "_" + oldKeyName; + + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, keyName) + .withMethod(HttpMethod.PUT) + .withExpiration(expiration); + + String key = generatePresignedUrlRequest.getKey(); + + return PresignedUrlUploadResponse.builder() + .url(amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString()) + .keyName(key) + .build(); + }) + .collect(Collectors.toList()); + + return PresignedUrlUploadResponseList.builder().presignedUrlUploadResponses(responses).build(); + } + + public String generateStaticUrl(String keyName) { + return "https://" + bucket + ".s3." + amazonS3.getRegionName() + ".amazonaws.com/" + keyName; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/scheduler/PlanScheduler.java b/src/main/java/com/dev/moim/global/scheduler/PlanScheduler.java new file mode 100644 index 00000000..c2b63df7 --- /dev/null +++ b/src/main/java/com/dev/moim/global/scheduler/PlanScheduler.java @@ -0,0 +1,60 @@ +package com.dev.moim.global.scheduler; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.moim.repository.IndividualPlanRepository; +import com.dev.moim.domain.moim.repository.UserPlanRepository; +import com.dev.moim.domain.moim.repository.UserTodoRepository; +import com.dev.moim.global.firebase.service.FcmService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional +public class PlanScheduler { + + private final IndividualPlanRepository individualPlanRepository; + private final UserPlanRepository userPlanRepository; + private final UserTodoRepository userTodoRepository; + private final UserRepository userRepository; + private final AlarmService alarmService; + private final FcmService fcmService; + + @Scheduled(cron = "0 0 9 * * *") + public void notifyDailyPlanCnt() { + + List userList = userRepository.findAllByIsPushAlarmTrueAndDeviceIdNotNull(); + + userList.forEach(user -> { + LocalDateTime startOfDay = LocalDate.now().atStartOfDay(); + LocalDateTime endOfDay = LocalDate.now().atTime(23, 59, 59, 999999000);; + + int individualPlanCnt = individualPlanRepository.countByUserAndDateBetween(user, startOfDay, endOfDay); + int moimPlanCnt = userPlanRepository.countPlansByUserAndDateBetween(user, startOfDay, endOfDay); + int todoPlanCnt = userTodoRepository.countByUserAndTodoDueDateBetween(user, startOfDay, endOfDay); + + String content = String.format( + "개인 일정: %d, 모임 일정: %d, 오늘 마감인 할 일: %d", + individualPlanCnt, moimPlanCnt, todoPlanCnt + ); + + alarmService.saveAlarm(user, user, "오늘 예정된 일정", content, AlarmType.PUSH, AlarmDetailType.PLAN, null, null, null); + + if (user.getIsPushAlarm() && user.getDeviceId() != null) { + fcmService.sendPushNotification(user, "오늘 예정된 일정", content, AlarmDetailType.PLAN); + } + }); + } +} diff --git a/src/main/java/com/dev/moim/global/scheduler/TodoScheduler.java b/src/main/java/com/dev/moim/global/scheduler/TodoScheduler.java new file mode 100644 index 00000000..3920e902 --- /dev/null +++ b/src/main/java/com/dev/moim/global/scheduler/TodoScheduler.java @@ -0,0 +1,62 @@ +package com.dev.moim.global.scheduler; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.AlarmDetailType; +import com.dev.moim.domain.account.entity.enums.AlarmType; +import com.dev.moim.domain.account.service.AlarmService; +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.UserTodo; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.domain.moim.repository.TodoRepository; +import com.dev.moim.domain.moim.repository.UserTodoRepository; +import com.dev.moim.domain.moim.service.TodoCommandService; +import com.dev.moim.global.firebase.service.FcmService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional +public class TodoScheduler { + + private final TodoCommandService todoCommandService; + private final TodoRepository todoRepository; + private final UserTodoRepository userTodoRepository; + private final AlarmService alarmService; + private final FcmService fcmService; + + @Scheduled(cron = "0 0 0 * * *") + public void updateTodosToExpiredStatus() { + todoCommandService.updateExpiredTodosAndAssigneesStatus(); + } + + @Scheduled(cron = "0 0 16 * * *") + public void notifyDueTodos() { + + LocalDateTime tomorrow = LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + LocalDateTime endOfTomorrow = tomorrow.withHour(23).withMinute(59).withSecond(59).withNano(999999999); + + List dueTomorrowTodoList = todoRepository.findAllByDueDateBetween(tomorrow, endOfTomorrow); + + dueTomorrowTodoList.forEach(todo -> { + List userTodoList = userTodoRepository.findAllByTodoIdAndStatusNot(todo.getId(), TodoAssigneeStatus.COMPLETE); + + userTodoList.forEach(userTodo -> { + User assignee = userTodo.getUser(); + + alarmService.saveAlarm(todo.getWriter(), assignee, "마감 기한이 하루 남았습니다.", todo.getTitle(), AlarmType.PUSH, AlarmDetailType.TODO, todo.getMoim().getId(), null, null); + + if (assignee.getIsPushAlarm() && assignee.getDeviceId() != null) { + fcmService.sendPushNotification(assignee, "마감 기한이 하루 남았습니다.", todo.getTitle(), AlarmDetailType.TODO); + } + }); + }); + } +} diff --git a/src/main/java/com/dev/moim/global/scheduler/UserMoimScheduler.java b/src/main/java/com/dev/moim/global/scheduler/UserMoimScheduler.java new file mode 100644 index 00000000..0571e62e --- /dev/null +++ b/src/main/java/com/dev/moim/global/scheduler/UserMoimScheduler.java @@ -0,0 +1,30 @@ +package com.dev.moim.global.scheduler; + +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +@Transactional +public class UserMoimScheduler { + + private final UserMoimRepository userMoimRepository; + + @Scheduled(cron = "0 0 0 * * *") + public void updateTodosToExpiredStatus() { + log.info("confirm userMoim delete 시작"); + + userMoimRepository.deleteAllByConfirmUserMoim(List.of(JoinStatus.LOADING, JoinStatus.COMPLETE)); + + log.info("confirm userMoim delete 종료"); + } + +} diff --git a/src/main/java/com/dev/moim/global/security/annotation/AuthUser.java b/src/main/java/com/dev/moim/global/security/annotation/AuthUser.java new file mode 100644 index 00000000..b439174a --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/annotation/AuthUser.java @@ -0,0 +1,13 @@ +package com.dev.moim.global.security.annotation; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Parameter(name = "user", hidden = true) +public @interface AuthUser {} diff --git a/src/main/java/com/dev/moim/global/security/annotation/AuthUserArgumentResolver.java b/src/main/java/com/dev/moim/global/security/annotation/AuthUserArgumentResolver.java new file mode 100644 index 00000000..c774c382 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/annotation/AuthUserArgumentResolver.java @@ -0,0 +1,71 @@ +package com.dev.moim.global.security.annotation; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.security.util.JwtUtil; +import jakarta.annotation.Nonnull; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.util.Date; +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { + + private final UserRepository userRepository; + private final RedisUtil redisUtil; + private final JwtUtil jwtUtil; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthUser.class) && parameter.getParameterType().equals(User.class); + } + + @Override + public Object resolveArgument( + @Nonnull MethodParameter parameter, + ModelAndViewContainer mavContainer, + @Nonnull NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws Exception { + + HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + + if (httpServletRequest == null) { + throw new AuthException(HTTP_REQUEST_NULL); + } + + String accessToken = jwtUtil.resolveToken(httpServletRequest); + + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(authentication -> { + String userId = authentication.getName(); + User user = userRepository.findById(Long.valueOf(userId)) + .orElseThrow(() -> new AuthException(USER_NOT_FOUND)); + + if (user.getDeviceId() == null) { + Long now = new Date().getTime(); + Long expiration = jwtUtil.getExpiration(accessToken) - now; + redisUtil.setValue(accessToken, "deviceId_missing", expiration); + + throw new AuthException(FCM_TOKEN_REQUIRED); + } + return user; + }).orElseThrow(() -> new AuthException(AUTH_INVALID_TOKEN)); + } +} diff --git a/src/main/java/com/dev/moim/global/security/annotation/ExtractToken.java b/src/main/java/com/dev/moim/global/security/annotation/ExtractToken.java new file mode 100644 index 00000000..82c0888a --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/annotation/ExtractToken.java @@ -0,0 +1,10 @@ +package com.dev.moim.global.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ExtractToken {} diff --git a/src/main/java/com/dev/moim/global/security/annotation/ExtractTokenArgumentResolver.java b/src/main/java/com/dev/moim/global/security/annotation/ExtractTokenArgumentResolver.java new file mode 100644 index 00000000..015ff517 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/annotation/ExtractTokenArgumentResolver.java @@ -0,0 +1,44 @@ +package com.dev.moim.global.security.annotation; + +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.security.util.JwtUtil; +import jakarta.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static com.dev.moim.global.common.code.status.ErrorStatus.MISSING_AUTHORIZATION_HEADER; + +@Component +@RequiredArgsConstructor +public class ExtractTokenArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtUtil jwtUtil; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(String.class) && parameter.hasParameterAnnotation(ExtractToken.class); + } + + @Override + public Object resolveArgument( + @Nonnull MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws Exception { + + String authorizationHeader = webRequest.getHeader("Authorization"); + if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { + throw new AuthException(MISSING_AUTHORIZATION_HEADER); + } + + String refreshToken = authorizationHeader.substring(7); + jwtUtil.isTokenValid(refreshToken); + return refreshToken; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/config/SecurityConfig.java b/src/main/java/com/dev/moim/global/security/config/SecurityConfig.java new file mode 100644 index 00000000..8fd2f899 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/config/SecurityConfig.java @@ -0,0 +1,171 @@ +package com.dev.moim.global.security.config; + +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.domain.user.service.UserCommandService; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.common.code.status.SuccessStatus; +import com.dev.moim.global.firebase.service.FcmQueryService; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.config.CorsConfig; +import com.dev.moim.global.security.exception.JwtAccessDeniedHandler; +import com.dev.moim.global.security.exception.JwtAuthenticationEntryPoint; +import com.dev.moim.global.security.filter.*; +import com.dev.moim.global.security.principal.PrincipalDetailsService; +import com.dev.moim.global.security.service.NaverLoginService; +import com.dev.moim.global.security.service.OIDCService; +import com.dev.moim.global.util.HttpResponseUtil; +import com.dev.moim.global.security.util.JwtUtil; +import com.dev.moim.global.security.util.NaverLoginAuthenticationProvider; +import com.dev.moim.global.security.util.OIDCAuthenticationProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; + +import java.util.Arrays; + +@Configuration +// @EnableWebSecurity(debug = true) +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final OIDCService oidcService; + private final NaverLoginService naverLoginService; + private final AuthenticationConfiguration authenticationConfiguration; + private final PrincipalDetailsService principalDetailsService; + private final ApplicationEventPublisher eventPublisher; + private final UserCommandService userCommandService; + private final UserQueryService userQueryService; + private final FcmQueryService fcmQueryService; + private final Environment environment; + + @Bean + public AuthenticationManager authenticationManager() throws Exception { + + ProviderManager authenticationManager = null; + try { + authenticationManager = (ProviderManager) authenticationConfiguration.getAuthenticationManager(); + } catch (Exception e) { + throw new RuntimeException(e); + } + authenticationManager.getProviders().add(daoAuthenticationProvider()); + authenticationManager.getProviders().add(oidcAuthenticationProvider()); + authenticationManager.getProviders().add(naverLoginAuthenticationProvider()); + + return authenticationConfiguration.getAuthenticationManager(); + } + + public DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(principalDetailsService); + provider.setPasswordEncoder(passwordEncoder()); + provider.setHideUserNotFoundExceptions(false); + return provider; + } + + public OIDCAuthenticationProvider oidcAuthenticationProvider() { + return new OIDCAuthenticationProvider(oidcService); + } + + public NaverLoginAuthenticationProvider naverLoginAuthenticationProvider() { + return new NaverLoginAuthenticationProvider(naverLoginService); + } + + private static final String[] allowUrls = { + "/swagger-ui/**", + "/v3/**", + "/api-docs/**", + "/api/v1/auth/join/**", + "/api/v1/auth/emails/**", + "/api/v1/auth/reissueToken/**", + "/health", + "/api/v1/auth/password/**", + "/api/v1/regions/**" + }; + + private static final String[] releaseAllowUrls = { + "/api/v1/auth/join/**", + "/api/v1/auth/emails/**", + "/api/v1/auth/reissueToken/**", + "/health", + "/api/v1/auth/password/**", + "/api/v1/regions/**" + }; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, JwtUtil jwtUtil, RedisUtil redisUtil, UserRepository userRepository) throws Exception { + + http.csrf(AbstractHttpConfigurer::disable); + http.cors(cors -> cors + .configurationSource(CorsConfig.apiConfigurationSource())); + + http.formLogin(AbstractHttpConfigurer::disable); + http.httpBasic(AbstractHttpConfigurer::disable); + + http.headers(headers -> headers + .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); + + http.sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy((SessionCreationPolicy.STATELESS))); + + http.exceptionHandling(configurer -> configurer + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + .accessDeniedHandler(jwtAccessDeniedHandler)); + + http.authorizeHttpRequests(requests -> requests + .requestMatchers(Arrays.asList(environment.getActiveProfiles()).contains("release") ? releaseAllowUrls : allowUrls).permitAll() + .requestMatchers("/**").authenticated() + .anyRequest().permitAll()); + + CustomLoginFilter customLoginFilter = new CustomLoginFilter( + authenticationManager(), jwtUtil, redisUtil, eventPublisher, fcmQueryService); + customLoginFilter.setFilterProcessesUrl("/api/v1/auth/login"); + + OAuthLoginFilter oAuthLoginFilter = new OAuthLoginFilter( + jwtUtil, redisUtil, authenticationManager(), userRepository, eventPublisher, fcmQueryService); + + http.addFilterAt(customLoginFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterAt(oAuthLoginFilter, UsernamePasswordAuthenticationFilter.class); + http.addFilterBefore(new JwtFilter(jwtUtil,redisUtil, Arrays.asList(environment.getActiveProfiles()).contains("release") ? releaseAllowUrls : allowUrls), CustomLoginFilter.class); + http.addFilterBefore(new JwtExceptionFilter(Arrays.asList(environment.getActiveProfiles()).contains("release") ? releaseAllowUrls : allowUrls), JwtFilter.class); + + http.logout(logout -> logout + .logoutUrl("/api/v1/auth/logout") + .addLogoutHandler( + new CustomLogoutHandler(jwtUtil, redisUtil, userCommandService, userQueryService)) + .logoutSuccessHandler((request, response, authentication) -> HttpResponseUtil.setSuccessResponse( + response, + SuccessStatus._OK, + "로그아웃 성공") + ) + ); + http.addFilterAfter( + new LogoutFilter( + (request, response, authentication) -> HttpResponseUtil.setSuccessResponse(response, SuccessStatus._OK, "로그아웃 성공"), + new CustomLogoutHandler(jwtUtil, redisUtil, userCommandService, userQueryService)), + JwtFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/dev/moim/global/security/dto/NaverUserInfo.java b/src/main/java/com/dev/moim/global/security/dto/NaverUserInfo.java new file mode 100644 index 00000000..557aa638 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/dto/NaverUserInfo.java @@ -0,0 +1,31 @@ +package com.dev.moim.global.security.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverUserInfo { + + private String resultcode; + private String message; + private Response response; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class Response { + private String email; + private String nickname; + private String profile_image; + private String age; + private String gender; + private String id; + private String name; + private String birthday; + private String birthyear; + private String mobile; + } +} diff --git a/src/main/java/com/dev/moim/global/security/event/CustomAuthenticationSuccessEvent.java b/src/main/java/com/dev/moim/global/security/event/CustomAuthenticationSuccessEvent.java new file mode 100644 index 00000000..3ab5b821 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/event/CustomAuthenticationSuccessEvent.java @@ -0,0 +1,24 @@ +package com.dev.moim.global.security.event; + +import com.dev.moim.global.security.principal.PrincipalDetails; +import org.springframework.context.ApplicationEvent; + +public class CustomAuthenticationSuccessEvent extends ApplicationEvent { + private final PrincipalDetails principalDetails; + private final String fcmToken; + + public CustomAuthenticationSuccessEvent(PrincipalDetails principalDetails, String fcmToken) { + super(principalDetails); + this.principalDetails = principalDetails; + this.fcmToken = fcmToken; + } + + public PrincipalDetails getPrincipalDetails() { + return principalDetails; + } + + public String getFcmToken() { + return fcmToken; + } +} + diff --git a/src/main/java/com/dev/moim/global/security/exception/JwtAccessDeniedHandler.java b/src/main/java/com/dev/moim/global/security/exception/JwtAccessDeniedHandler.java new file mode 100644 index 00000000..2901d789 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/exception/JwtAccessDeniedHandler.java @@ -0,0 +1,35 @@ +package com.dev.moim.global.security.exception; + +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.util.HttpResponseUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_INSUFFICIENT_PERMISSION; + +@Slf4j +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + + BaseResponse errorResponse = BaseResponse.onFailure( + USER_INSUFFICIENT_PERMISSION.getCode(), + USER_INSUFFICIENT_PERMISSION.getMessage(), + null); + + HttpResponseUtil.setErrorResponse(response, HttpStatus.FORBIDDEN, errorResponse); + } +} diff --git a/src/main/java/com/dev/moim/global/security/exception/JwtAuthenticationEntryPoint.java b/src/main/java/com/dev/moim/global/security/exception/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..9b83ac90 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/exception/JwtAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package com.dev.moim.global.security.exception; + +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.util.HttpResponseUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_AUTHENTICATION_FAIL; + +@Slf4j +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + log.error("** JwtAuthenticationEntryPoint **"); + + BaseResponse errorResponse = BaseResponse.onFailure( + USER_AUTHENTICATION_FAIL.getCode(), + USER_AUTHENTICATION_FAIL.getMessage(), + null); + + HttpResponseUtil.setErrorResponse(response, HttpStatus.UNAUTHORIZED, errorResponse); + } +} diff --git a/src/main/java/com/dev/moim/global/security/feign/config/AppleFeignConfiguration.java b/src/main/java/com/dev/moim/global/security/feign/config/AppleFeignConfiguration.java new file mode 100644 index 00000000..619b1a1a --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/config/AppleFeignConfiguration.java @@ -0,0 +1,22 @@ +package com.dev.moim.global.security.feign.config; + +import com.dev.moim.global.security.feign.decoder.FeignErrorDecoder; +import feign.Logger; +import feign.RequestInterceptor; +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppleFeignConfiguration { + @Bean(name = "appleRequestInterceptor") + public RequestInterceptor requestInterceptor() { + return template -> template.header("Content-Type", "application/x-www-form-urlencoded"); + } + + @Bean(name = "appleErrorDecoder") + public ErrorDecoder errorDecoder() {return new FeignErrorDecoder();} + + @Bean(name = "appleFeignLoggerLevel") + Logger.Level feignLoggerLevel() {return Logger.Level.FULL;} +} diff --git a/src/main/java/com/dev/moim/global/security/feign/config/GoogleFeignConfiguration.java b/src/main/java/com/dev/moim/global/security/feign/config/GoogleFeignConfiguration.java new file mode 100644 index 00000000..235eccac --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/config/GoogleFeignConfiguration.java @@ -0,0 +1,26 @@ +package com.dev.moim.global.security.feign.config; + +import com.dev.moim.global.security.feign.decoder.FeignErrorDecoder; +import feign.Logger; +import feign.RequestInterceptor; +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GoogleFeignConfiguration { + @Bean(name = "googleRequestInterceptor") + public RequestInterceptor requestInterceptor() { + return template -> template.header("Content-Type", "application/json; charset=UTF-8"); + } + + @Bean(name = "googleErrorDecoder") + public ErrorDecoder errorDecoder() { + return new FeignErrorDecoder(); + } + + @Bean(name = "googleFeignLoggerLevel") + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} diff --git a/src/main/java/com/dev/moim/global/security/feign/config/KakaoFeignConfiguration.java b/src/main/java/com/dev/moim/global/security/feign/config/KakaoFeignConfiguration.java new file mode 100644 index 00000000..92c7fdc8 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/config/KakaoFeignConfiguration.java @@ -0,0 +1,26 @@ +package com.dev.moim.global.security.feign.config; + +import com.dev.moim.global.security.feign.decoder.FeignErrorDecoder; +import feign.Logger; +import feign.RequestInterceptor; +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KakaoFeignConfiguration { + @Bean(name = "kakaoRequestInterceptor") + public RequestInterceptor requestInterceptor() { + return template -> template.header("Content-Type", "application/x-www-form-urlencoded"); + } + + @Bean(name = "kakaoErrorDecoder") + public ErrorDecoder errorDecoder() { + return new FeignErrorDecoder(); + } + + @Bean(name = "kakaoFeignLoggerLevel") + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} diff --git a/src/main/java/com/dev/moim/global/security/feign/config/NaverFeignConfiguration.java b/src/main/java/com/dev/moim/global/security/feign/config/NaverFeignConfiguration.java new file mode 100644 index 00000000..fdd040ff --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/config/NaverFeignConfiguration.java @@ -0,0 +1,19 @@ +package com.dev.moim.global.security.feign.config; + +import com.dev.moim.global.security.feign.decoder.FeignErrorDecoder; +import feign.Logger; +import feign.codec.ErrorDecoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Slf4j +@Configuration +public class NaverFeignConfiguration { + + @Bean(name = "naverErrorDecoder") + public ErrorDecoder errorDecoder() {return new FeignErrorDecoder();} + + @Bean(name = "naverFeignLoggerLevel") + Logger.Level feignLoggerLevel() {return Logger.Level.FULL;} +} diff --git a/src/main/java/com/dev/moim/global/security/feign/config/OauthProperties.java b/src/main/java/com/dev/moim/global/security/feign/config/OauthProperties.java new file mode 100644 index 00000000..6f12d125 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/config/OauthProperties.java @@ -0,0 +1,55 @@ +package com.dev.moim.global.security.feign.config; + +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.global.error.handler.AuthException; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Getter +@AllArgsConstructor +@ConfigurationProperties(prefix = "oauth") +public class OauthProperties { + + private OAuthSecret kakao; + private OAuthSecret google; + private OAuthSecret apple; + + @Getter + @Setter + public static class OAuthSecret { + private String baseUrl; + private String appKey; + } + + public String getBaseUrl(Provider provider) { + return switch (provider) { + case KAKAO -> getOAuthSecret(kakao).getBaseUrl(); + case GOOGLE -> getOAuthSecret(google).getBaseUrl(); + case APPLE -> getOAuthSecret(apple).getBaseUrl(); + default -> throw new AuthException(PROVIDER_NOT_FOUND); + }; + } + + public String getAppKey(Provider provider) { + return switch (provider) { + case KAKAO -> getOAuthSecret(kakao).getAppKey(); + case GOOGLE -> getOAuthSecret(google).getAppKey(); + case APPLE -> getOAuthSecret(apple).getAppKey(); + default -> throw new AuthException(PROVIDER_NOT_FOUND); + }; + } + + private OAuthSecret getOAuthSecret(OAuthSecret secret) { + if (secret == null || secret.getBaseUrl() == null || secret.getAppKey() == null) { + log.error("OAuth 관련 환경 변수 누락"); + throw new AuthException(OAUTH_SECRET_NOT_FOUND); + } + return secret; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/feign/decoder/FeignErrorDecoder.java b/src/main/java/com/dev/moim/global/security/feign/decoder/FeignErrorDecoder.java new file mode 100644 index 00000000..c4ff1355 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/decoder/FeignErrorDecoder.java @@ -0,0 +1,30 @@ +package com.dev.moim.global.security.feign.decoder; + +import com.dev.moim.global.error.handler.FeignException; +import feign.Response; +import feign.codec.ErrorDecoder; +import lombok.extern.slf4j.Slf4j; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +public class FeignErrorDecoder implements ErrorDecoder { + + @Override + public Exception decode(String methodKey, Response response) { + + log.error("response.status : {}", response.status()); + log.error("response.body : {}", response.body()); + log.error("response : {}", response); + + if (response.status() == 400) { + log.error("ERROR STATUS 400"); + return new FeignException(USER_PROPERTY_NOT_FOUND); + } else if (response.status() == 401) { + log.error("ERROR STATUS 401"); + return new FeignException(OAUTH_INVALID_TOKEN); + } else { + return new FeignException(FEIGN_UNKNOWN_ERROR); + } + } +} diff --git a/src/main/java/com/dev/moim/global/security/feign/dto/OIDCPublicKeyDTO.java b/src/main/java/com/dev/moim/global/security/feign/dto/OIDCPublicKeyDTO.java new file mode 100644 index 00000000..1f3d535c --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/dto/OIDCPublicKeyDTO.java @@ -0,0 +1,10 @@ +package com.dev.moim.global.security.feign.dto; + +public record OIDCPublicKeyDTO( + String kid, + String alg, + String use, + String n, + String e +) { +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/feign/dto/OIDCPublicKeyListDTO.java b/src/main/java/com/dev/moim/global/security/feign/dto/OIDCPublicKeyListDTO.java new file mode 100644 index 00000000..0992ff12 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/dto/OIDCPublicKeyListDTO.java @@ -0,0 +1,8 @@ +package com.dev.moim.global.security.feign.dto; + +import java.util.List; + +public record OIDCPublicKeyListDTO ( + List keys +) { +} diff --git a/src/main/java/com/dev/moim/global/security/feign/request/AppleFeign.java b/src/main/java/com/dev/moim/global/security/feign/request/AppleFeign.java new file mode 100644 index 00000000..948c8643 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/request/AppleFeign.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.security.feign.request; + +import com.dev.moim.global.security.feign.config.AppleFeignConfiguration; +import com.dev.moim.global.security.feign.dto.OIDCPublicKeyListDTO; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient( + name = "AppleFeign", + url = "https://appleid.apple.com", + configuration = AppleFeignConfiguration.class +) +public interface AppleFeign { + @Cacheable(cacheNames = "APPLE", cacheManager = "oidcCacheManager") + @GetMapping("/auth/keys") + OIDCPublicKeyListDTO getAppleOIDCOpenKeys(); +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/feign/request/GoogleFeign.java b/src/main/java/com/dev/moim/global/security/feign/request/GoogleFeign.java new file mode 100644 index 00000000..9196cebd --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/request/GoogleFeign.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.security.feign.request; + +import com.dev.moim.global.security.feign.config.GoogleFeignConfiguration; +import com.dev.moim.global.security.feign.dto.OIDCPublicKeyListDTO; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient( + name = "GoogleFeign", + url = "https://www.googleapis.com", + configuration = GoogleFeignConfiguration.class +) +public interface GoogleFeign { + @Cacheable(cacheNames = "GOOGLE", cacheManager = "oidcCacheManager") + @GetMapping("/oauth2/v3/certs") + OIDCPublicKeyListDTO getGoogleOIDCOpenKeys(); +} diff --git a/src/main/java/com/dev/moim/global/security/feign/request/KakaoFeign.java b/src/main/java/com/dev/moim/global/security/feign/request/KakaoFeign.java new file mode 100644 index 00000000..296c346b --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/request/KakaoFeign.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.security.feign.request; + +import com.dev.moim.global.security.feign.config.KakaoFeignConfiguration; +import com.dev.moim.global.security.feign.dto.OIDCPublicKeyListDTO; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +@FeignClient( + name = "KakaoFeign", + url = "https://kauth.kakao.com", + configuration = KakaoFeignConfiguration.class +) +public interface KakaoFeign { + @Cacheable(cacheNames = "KAKAO", cacheManager = "oidcCacheManager") + @GetMapping("/.well-known/jwks.json") + OIDCPublicKeyListDTO getKakaoOIDCOpenKeys(); +} diff --git a/src/main/java/com/dev/moim/global/security/feign/request/NaverFeign.java b/src/main/java/com/dev/moim/global/security/feign/request/NaverFeign.java new file mode 100644 index 00000000..884f783a --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/feign/request/NaverFeign.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.security.feign.request; + +import com.dev.moim.global.security.feign.config.NaverFeignConfiguration; +import com.dev.moim.global.security.dto.NaverUserInfo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient( + name = "naverFeign", + url = "https://openapi.naver.com", + configuration = NaverFeignConfiguration.class +) +public interface NaverFeign { + @GetMapping("/v1/nid/me") + NaverUserInfo getUserInfo(@RequestHeader(name = "Authorization") String accessToken); +} diff --git a/src/main/java/com/dev/moim/global/security/filter/CustomLoginFilter.java b/src/main/java/com/dev/moim/global/security/filter/CustomLoginFilter.java new file mode 100644 index 00000000..bb9e88d3 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/filter/CustomLoginFilter.java @@ -0,0 +1,114 @@ +package com.dev.moim.global.security.filter; + +import com.dev.moim.domain.account.dto.LoginRequest; +import com.dev.moim.domain.account.dto.TokenResponse; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.firebase.service.FcmQueryService; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.security.event.CustomAuthenticationSuccessEvent; +import com.dev.moim.global.security.principal.PrincipalDetails; +import com.dev.moim.global.util.HttpRequestUtil; +import com.dev.moim.global.util.HttpResponseUtil; +import com.dev.moim.global.security.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.io.IOException; +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; +import static com.dev.moim.global.common.code.status.SuccessStatus._OK; + +@Slf4j +@RequiredArgsConstructor +public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final ApplicationEventPublisher eventPublisher; + private final FcmQueryService fcmQueryService; + + @Override + public Authentication attemptAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response) throws AuthenticationException { + + LoginRequest logInRequest = HttpRequestUtil.readBody(request, LoginRequest.class); + String fcmToken = Optional.ofNullable(logInRequest.fcmToken()) + .filter(token -> !token.trim().isEmpty()) + .orElseThrow(() -> new AuthException(FCM_TOKEN_REQUIRED)); + // fcmQueryService.isTokenValid("MOIM", fcmToken); + request.setAttribute("fcmToken", fcmToken); + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + UsernamePasswordAuthenticationToken.unauthenticated(logInRequest.email(), logInRequest.password()); + + return authenticationManager.authenticate(usernamePasswordAuthenticationToken); + } + + @Override + protected void successfulAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain chain, + @NonNull Authentication authResult) throws IOException{ + SecurityContextHolder.getContext().setAuthentication(authResult); + + PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal(); + + String accessToken = jwtUtil.createAccessToken(principalDetails); + String refreshToken = jwtUtil.createRefreshToken(principalDetails); + + try { + redisUtil.setValue(principalDetails.user().getId().toString(), refreshToken, jwtUtil.getRefreshTokenValiditySec()); + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } + + eventPublisher.publishEvent(new CustomAuthenticationSuccessEvent(principalDetails, request.getAttribute("fcmToken").toString())); + + HttpResponseUtil.setSuccessResponse(response, _OK, new TokenResponse(accessToken, refreshToken, principalDetails.getProvider())); + } + + @Override + protected void unsuccessfulAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull AuthenticationException failed) throws IOException { + ErrorStatus errorStatus; + + if (failed instanceof UsernameNotFoundException) { + errorStatus = USER_UNREGISTERED; + } else if (failed instanceof BadCredentialsException) { + errorStatus = BAD_CREDENTIALS; + } else { + errorStatus = AUTHENTICATION_FAILED; + } + log.error("[ERROR] : {}", errorStatus); + + BaseResponse errorResponse = BaseResponse.onFailure( + errorStatus.getCode(), + errorStatus.getMessage(), + null); + + HttpResponseUtil.setErrorResponse(response, HttpStatus.UNAUTHORIZED, errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/filter/CustomLogoutHandler.java b/src/main/java/com/dev/moim/global/security/filter/CustomLogoutHandler.java new file mode 100644 index 00000000..cf18d4bb --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/filter/CustomLogoutHandler.java @@ -0,0 +1,53 @@ +package com.dev.moim.global.security.filter; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.service.UserCommandService; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.security.util.JwtUtil; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; + +import java.util.Date; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@RequiredArgsConstructor +@Slf4j +public class CustomLogoutHandler implements LogoutHandler { + + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final UserCommandService userCommandService; + private final UserQueryService userQueryService; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + + try { + String accessToken = jwtUtil.resolveToken(request); + String userId = jwtUtil.getUserId(accessToken); + + User user = userQueryService.findUserById(Long.valueOf(userId)) + .orElseThrow(() ->new AuthException(USER_NOT_FOUND)); + userCommandService.fcmSignOut(user); + + Long now = new Date().getTime(); + Long expiration = jwtUtil.getExpiration(accessToken) - now; + redisUtil.setValue(accessToken, "logout", expiration); + + redisUtil.deleteValue(userId); + } catch (ExpiredJwtException e) { + throw new AuthException(AUTH_EXPIRED_TOKEN); + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/filter/JwtExceptionFilter.java b/src/main/java/com/dev/moim/global/security/filter/JwtExceptionFilter.java new file mode 100644 index 00000000..0dd0e4dc --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/filter/JwtExceptionFilter.java @@ -0,0 +1,54 @@ +package com.dev.moim.global.security.filter; + +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.error.handler.FeignException; +import com.dev.moim.global.util.HttpResponseUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; + +@Slf4j +@RequiredArgsConstructor +public class JwtExceptionFilter extends OncePerRequestFilter { + + private final String[] excludePath; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (AuthException | FeignException e) { + log.error("[ERROR] : {}", e.getCode()); + + ErrorStatus errorStatus = (ErrorStatus) e.getCode(); + + BaseResponse errorResponse = BaseResponse.onFailure( + errorStatus.getCode(), + errorStatus.getMessage(), + null); + + HttpResponseUtil.setErrorResponse(response, e.getErrorReasonHttpStatus().getHttpStatus(), errorResponse); + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getRequestURI(); + + return Arrays.stream(excludePath).anyMatch(path::startsWith); + } +} diff --git a/src/main/java/com/dev/moim/global/security/filter/JwtFilter.java b/src/main/java/com/dev/moim/global/security/filter/JwtFilter.java new file mode 100644 index 00000000..5b8d9a6e --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/filter/JwtFilter.java @@ -0,0 +1,78 @@ +package com.dev.moim.global.security.filter; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.security.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final String[] excludePath; + + @Override + public void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + String accessToken = jwtUtil.resolveToken(request); + + if (accessToken == null) { + filterChain.doFilter(request, response); + return; + } + + try { + String redisValue = redisUtil.getValue(accessToken); + + if (Objects.equals(redisValue, "deviceId_missing")) { + throw new AuthException(FCM_TOKEN_REQUIRED); + } + + if (Objects.equals(redisValue, "logout")) { + throw new AuthException(LOGOUT_ACCESS_TOKEN); + } + + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } + + if(jwtUtil.isTokenValid(accessToken)) { + Authentication authentication = jwtUtil.getAuthentication(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + + } else { + throw new AuthException(ErrorStatus.AUTH_INVALID_TOKEN); + } + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getRequestURI(); + + return Arrays.stream(excludePath).anyMatch(path::startsWith); + } +} diff --git a/src/main/java/com/dev/moim/global/security/filter/OAuthLoginFilter.java b/src/main/java/com/dev/moim/global/security/filter/OAuthLoginFilter.java new file mode 100644 index 00000000..230d5c41 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/filter/OAuthLoginFilter.java @@ -0,0 +1,116 @@ +package com.dev.moim.global.security.filter; + +import com.dev.moim.domain.account.dto.LoginResponseDTO; +import com.dev.moim.domain.account.dto.OAuthLoginRequest; +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.domain.account.repository.UserRepository; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.firebase.service.FcmQueryService; +import com.dev.moim.global.redis.util.RedisUtil; +import com.dev.moim.global.security.event.CustomAuthenticationSuccessEvent; +import com.dev.moim.global.security.principal.PrincipalDetails; +import com.dev.moim.global.security.util.*; +import com.dev.moim.global.util.HttpRequestUtil; +import com.dev.moim.global.util.HttpResponseUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import java.io.IOException; +import java.util.Optional; + +import static com.dev.moim.domain.account.entity.enums.Provider.NAVER; +import static com.dev.moim.domain.account.entity.enums.Provider.UNREGISTERED; +import static com.dev.moim.global.common.code.status.ErrorStatus.FCM_TOKEN_REQUIRED; +import static com.dev.moim.global.common.code.status.ErrorStatus.REDIS_CONNECTION_ERROR; +import static com.dev.moim.global.common.code.status.SuccessStatus.UNREGISTERED_OAUTH_LOGIN_USER; +import static com.dev.moim.global.common.code.status.SuccessStatus._OK; + +@Slf4j +public class OAuthLoginFilter extends AbstractAuthenticationProcessingFilter { + + private final JwtUtil jwtUtil; + private final RedisUtil redisUtil; + private final AuthenticationManager authenticationManager; + private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; + private final FcmQueryService fcmQueryService; + + public OAuthLoginFilter( + JwtUtil jwtUtil, + RedisUtil redisUtil, + AuthenticationManager authenticationManager, + UserRepository userRepository, + ApplicationEventPublisher eventPublisher, + FcmQueryService fcmQueryService) { + super(new AntPathRequestMatcher("/api/v1/auth/oAuth")); + this.jwtUtil = jwtUtil; + this.redisUtil = redisUtil; + this.authenticationManager = authenticationManager; + this.userRepository = userRepository; + this.eventPublisher = eventPublisher; + this.fcmQueryService = fcmQueryService; + } + + @Override + public Authentication attemptAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response) throws IOException, ServletException, AuthException { + + OAuthLoginRequest oAuthLoginRequest = HttpRequestUtil.readBody(request, OAuthLoginRequest.class); + String fcmToken = Optional.ofNullable(oAuthLoginRequest.fcmToken()) + .filter(token -> !token.trim().isEmpty()) + .orElseThrow(() -> new AuthException(FCM_TOKEN_REQUIRED)); + // fcmQueryService.isTokenValid("MOIM", fcmToken); + request.setAttribute("fcmToken", fcmToken); + + Provider provider = oAuthLoginRequest.provider(); + String token = oAuthLoginRequest.token(); + + if (provider == NAVER) { + return authenticationManager.authenticate(new NaverLoginAuthenticationToken(provider, null, token, null)); + } else { + return authenticationManager.authenticate(new OIDCAuthenticationToken(provider, null, token, null)); + } + } + + @Override + protected void successfulAuthentication( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain chain, + @NonNull Authentication authResult) throws IOException, ServletException { + + Optional user = userRepository.findByProviderIdAndProvider(authResult.getCredentials().toString(), (Provider) authResult.getPrincipal()); + + if (user.isPresent()) { + User existingUser = user.get(); + PrincipalDetails principalDetails = new PrincipalDetails(existingUser); + + String accessToken = jwtUtil.createAccessToken(principalDetails); + String refreshToken = jwtUtil.createRefreshToken(principalDetails); + + try { + redisUtil.setValue(principalDetails.user().getId().toString(), refreshToken, jwtUtil.getRefreshTokenValiditySec()); + } catch (RedisConnectionFailureException e) { + throw new AuthException(REDIS_CONNECTION_ERROR); + } + eventPublisher.publishEvent(new CustomAuthenticationSuccessEvent(principalDetails, request.getAttribute("fcmToken").toString())); + + HttpResponseUtil.setSuccessResponse(response, _OK, new LoginResponseDTO(accessToken, refreshToken, principalDetails.getProvider(), authResult.getName())); + } else { + log.info("신규 유저 : 추가 정보 입력 필요"); + HttpResponseUtil.setSuccessResponse(response, UNREGISTERED_OAUTH_LOGIN_USER, new LoginResponseDTO(null, null, UNREGISTERED, authResult.getName())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/principal/PrincipalDetails.java b/src/main/java/com/dev/moim/global/security/principal/PrincipalDetails.java new file mode 100644 index 00000000..48bad082 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/principal/PrincipalDetails.java @@ -0,0 +1,61 @@ +package com.dev.moim.global.security.principal; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.entity.enums.Provider; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public record PrincipalDetails(User user) implements UserDetails { + + @Override + public Collection getAuthorities() { + Collection collections = new ArrayList<>(); + collections.add(new SimpleGrantedAuthority(user.getUserRole().toString())); + + return collections; + } + + public Long getUserId() { + return user.getId(); + } + + public Provider getProvider() { + return user.getProvider(); + } + + public String getProviderId() { + return user.getProviderId(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public String getPassword() {return user.getPassword();} + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/security/principal/PrincipalDetailsService.java b/src/main/java/com/dev/moim/global/security/principal/PrincipalDetailsService.java new file mode 100644 index 00000000..e2d92ea7 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/principal/PrincipalDetailsService.java @@ -0,0 +1,29 @@ +package com.dev.moim.global.security.principal; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.account.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import static com.dev.moim.domain.account.entity.enums.Provider.LOCAL; +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_NOT_FOUND; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PrincipalDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmailAndProvider(email, LOCAL) + .orElseThrow(() -> new UsernameNotFoundException(USER_NOT_FOUND.getMessage())); + + return new PrincipalDetails(user); + } +} diff --git a/src/main/java/com/dev/moim/global/security/service/NaverLoginService.java b/src/main/java/com/dev/moim/global/security/service/NaverLoginService.java new file mode 100644 index 00000000..5d022c54 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/service/NaverLoginService.java @@ -0,0 +1,20 @@ +package com.dev.moim.global.security.service; + +import com.dev.moim.global.security.feign.request.NaverFeign; +import com.dev.moim.global.security.dto.NaverUserInfo; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class NaverLoginService { + + private final NaverFeign naverFeign; + + public NaverUserInfo getUserInfo(String oAuthToken) { + + return naverFeign.getUserInfo("bearer " + oAuthToken); + } +} diff --git a/src/main/java/com/dev/moim/global/security/service/OIDCService.java b/src/main/java/com/dev/moim/global/security/service/OIDCService.java new file mode 100644 index 00000000..734df4f9 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/service/OIDCService.java @@ -0,0 +1,93 @@ +package com.dev.moim.global.security.service; + +import com.dev.moim.domain.account.dto.OIDCDecodePayload; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.security.feign.config.OauthProperties; +import com.dev.moim.global.security.feign.dto.OIDCPublicKeyDTO; +import com.dev.moim.global.security.feign.dto.OIDCPublicKeyListDTO; +import com.dev.moim.global.security.feign.request.AppleFeign; +import com.dev.moim.global.security.feign.request.GoogleFeign; +import com.dev.moim.global.security.feign.request.KakaoFeign; +import com.dev.moim.global.security.util.JwtOIDCUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import static com.dev.moim.domain.account.entity.enums.Provider.*; +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@RequiredArgsConstructor +@Service +@Slf4j +public class OIDCService { + + private final KakaoFeign kakaoFeign; + private final GoogleFeign googleFeign; + private final AppleFeign appleFeign; + private final JwtOIDCUtil jwtOIDCUtil; + private final OauthProperties oauthProperties; + private final CacheManager oidcCacheManager; + + public OIDCDecodePayload getOIDCDecodePayload(Provider provider, String idToken) { + OIDCPublicKeyListDTO oidcPublicKeyList = getOidcPublicKeyList(provider); + + try { + return getPayloadFromIdToken( + idToken, + oauthProperties.getBaseUrl(provider), + oauthProperties.getAppKey(provider), + oidcPublicKeyList); + } catch (AuthException e) { + if (e.getCode() == OIDC_PUBLIC_KEY_NOT_FOUND) { + log.info("OIDC_PUBLIC_KEY_NOT_FOUND"); + + invalidateCache(provider); + + oidcPublicKeyList = getOidcPublicKeyList(provider); + + return getPayloadFromIdToken( + idToken, + oauthProperties.getBaseUrl(provider), + oauthProperties.getAppKey(provider), + oidcPublicKeyList); + } + throw new AuthException((ErrorStatus) e.getCode()); + } + } + + private OIDCPublicKeyListDTO getOidcPublicKeyList(Provider provider) { + if (provider.equals(KAKAO)) { + return kakaoFeign.getKakaoOIDCOpenKeys(); + } else if (provider.equals(GOOGLE)) { + return googleFeign.getGoogleOIDCOpenKeys(); + } else if (provider.equals(APPLE)) { + return appleFeign.getAppleOIDCOpenKeys(); + } else { + throw new AuthException(PROVIDER_NOT_FOUND); + } + } + + private void invalidateCache(Provider provider) { + Cache cache = oidcCacheManager.getCache(provider.toString()); + if (cache != null) { + cache.clear(); + log.info("캐시된 {} 공개키 리스트 clear", provider); + } + } + + public OIDCDecodePayload getPayloadFromIdToken(String token, String iss, String aud, OIDCPublicKeyListDTO oidcPublicKeyList) { + String kid = jwtOIDCUtil.getKidFromUnsignedTokenHeader(token, iss, aud); + + OIDCPublicKeyDTO oidcPublicKey = oidcPublicKeyList.keys().stream() + .filter(o -> o.kid().equals(kid)) + .findFirst() + .orElseThrow(() -> new AuthException(OIDC_PUBLIC_KEY_NOT_FOUND)); + + return jwtOIDCUtil.getOIDCTokenBody( + token, oidcPublicKey.n(), oidcPublicKey.e()); + } +} diff --git a/src/main/java/com/dev/moim/global/security/util/JwtOIDCUtil.java b/src/main/java/com/dev/moim/global/security/util/JwtOIDCUtil.java new file mode 100644 index 00000000..fbfbdad5 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/util/JwtOIDCUtil.java @@ -0,0 +1,108 @@ +package com.dev.moim.global.security.util; + +import com.dev.moim.domain.account.dto.OIDCDecodePayload; +import com.dev.moim.global.error.handler.AuthException; +import io.jsonwebtoken.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +public class JwtOIDCUtil { + + private final String KID = "kid"; + + public String getKidFromUnsignedTokenHeader(String token, String iss, String aud) { + return (String) getUnsignedTokenClaims(token, iss, aud).getHeader().get(KID); + } + + private Jwt getUnsignedTokenClaims(String token, String iss, String aud) { + + String unsignedToken = getUnsignedToken(token); + try { + return Jwts.parserBuilder() + .requireAudience(aud) + .requireIssuer(iss) + .build() + .parseClaimsJwt(unsignedToken); + } catch (ExpiredJwtException e) { + log.error("유저 인증 실패 : 토큰 만료 : {}", e.getMessage()); + throw new AuthException(ID_TOKEN_EXPIRED); + } catch (UnsupportedJwtException | + MalformedJwtException | + ClaimJwtException e) { + log.error("유저 인증 실패 : {}", e.getMessage()); + throw new AuthException(ID_TOKEN_INVALID); + } catch (JwtException e) { + log.error("유저 인증 실패 : JWT processing error: {}", e.getMessage()); + throw new AuthException(ID_TOKEN_INVALID); + } + } + + private String getUnsignedToken(String token) { + + String[] splitToken = token.split("\\."); + if (splitToken.length != 3) { + throw new AuthException(ID_TOKEN_INVALID); + } + return splitToken[0] + "." + splitToken[1] + "."; + } + + public OIDCDecodePayload getOIDCTokenBody(String token, String modulus, String exponent) { + + Claims body = getOIDCTokenJws(token, modulus, exponent).getBody(); + return new OIDCDecodePayload( + body.getIssuer(), + body.getAudience(), + body.getSubject(), + body.get("email", String.class) + ); + } + + private Jws getOIDCTokenJws(String token, String modulus, String exponent) { + try { + return Jwts.parserBuilder() + .setSigningKey(getRSAPublicKey(modulus, exponent)) + .build() + .parseClaimsJws(token); + } catch (ExpiredJwtException e) { + log.error("유저 인증 실패 : 토큰 만료 : {}", e.getMessage()); + throw new AuthException(ID_TOKEN_EXPIRED); + } catch (UnsupportedJwtException | + MalformedJwtException | + ClaimJwtException e) { + log.error("유저 인증 실패 : {}", e.getMessage()); + throw new AuthException(ID_TOKEN_INVALID); + } catch (JwtException e) { + log.error("유저 인증 실패 : JWT processing error: {}", e.getMessage()); + throw new AuthException(ID_TOKEN_INVALID); + } catch (Exception e) { + log.error("JWT 파싱 실패 : {}", e.getMessage()); + throw new AuthException(ID_TOKEN_INVALID); + } + } + + private Key getRSAPublicKey(String modulus, String exponent) + throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + byte[] decodeN = Base64.getUrlDecoder().decode(modulus); + byte[] decodeE = Base64.getUrlDecoder().decode(exponent); + BigInteger n = new BigInteger(1, decodeN); + BigInteger e = new BigInteger(1, decodeE); + + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e); + Key key = keyFactory.generatePublic(keySpec); + return key; + } +} diff --git a/src/main/java/com/dev/moim/global/security/util/JwtUtil.java b/src/main/java/com/dev/moim/global/security/util/JwtUtil.java new file mode 100644 index 00000000..fd23c234 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/util/JwtUtil.java @@ -0,0 +1,119 @@ +package com.dev.moim.global.security.util; + +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.security.principal.PrincipalDetails; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@Getter +public class JwtUtil { + + private final SecretKey secretKey; + private final Long accessTokenValiditySec; + private final Long refreshTokenValiditySec; + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public JwtUtil( + @Value("${spring.jwt.secret}") final String secretKey, + @Value("${spring.jwt.access-token-validity}") final Long accessTokenValiditySec, + @Value("${spring.jwt.refresh-token-validity}") final Long refreshTokenValiditySec) { + this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + this.accessTokenValiditySec = accessTokenValiditySec; + this.refreshTokenValiditySec = refreshTokenValiditySec; + } + + public String createAccessToken(PrincipalDetails principalDetails) { + return createToken(principalDetails, accessTokenValiditySec); + } + + public String createRefreshToken(PrincipalDetails principalDetails) { + return createToken(principalDetails, refreshTokenValiditySec); + } + + private String createToken( + PrincipalDetails principalDetails, Long validitySeconds) { + Instant issuedAt = Instant.now(); + Instant expirationTime = issuedAt.plusSeconds(validitySeconds); + + return Jwts.builder() + .setSubject(String.valueOf(principalDetails.getUserId())) + .claim("role", principalDetails.getAuthorities()) + .setIssuedAt(Date.from(issuedAt)) + .setExpiration(Date.from(expirationTime)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public String getUserId(String token) { + return getClaims(token).getBody().getSubject(); + } + + public Long getExpiration(String token) { + return getClaims(token).getBody().getExpiration().getTime(); + } + + public boolean isTokenValid(String token) throws AuthException { + try { + Jws claims = getClaims(token); + Date expiredDate = claims.getBody().getExpiration(); + Date now = new Date(); + return expiredDate.after(now); + } catch (ExpiredJwtException e) { + throw new AuthException(AUTH_EXPIRED_TOKEN); + } catch (SecurityException + | MalformedJwtException + | UnsupportedJwtException + | IllegalArgumentException e) { + throw new AuthException(AUTH_INVALID_TOKEN); + } + } + + private Jws getClaims(String token) { + return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); + } + + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + public Authentication getAuthentication (String token) { + Claims claims = getClaims(token).getBody(); + + Collection authorities = + Arrays.stream(claims.get("role").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + User principal = new User(claims.getSubject(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, token, authorities); + } +} diff --git a/src/main/java/com/dev/moim/global/security/util/NaverLoginAuthenticationProvider.java b/src/main/java/com/dev/moim/global/security/util/NaverLoginAuthenticationProvider.java new file mode 100644 index 00000000..19c19d41 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/util/NaverLoginAuthenticationProvider.java @@ -0,0 +1,36 @@ +package com.dev.moim.global.security.util; + +import com.dev.moim.global.error.handler.FeignException; +import com.dev.moim.global.security.dto.NaverUserInfo; +import com.dev.moim.global.security.service.NaverLoginService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import static com.dev.moim.domain.account.entity.enums.Provider.NAVER; + +@Slf4j +@RequiredArgsConstructor +@Component +public class NaverLoginAuthenticationProvider implements AuthenticationProvider { + + private final NaverLoginService naverLoginService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException, FeignException { + + NaverLoginAuthenticationToken naverAuthenticationToken = (NaverLoginAuthenticationToken) authentication; + + NaverUserInfo naverUserInfo = naverLoginService.getUserInfo(naverAuthenticationToken.getOAuthAccessToken()); + + return new NaverLoginAuthenticationToken(NAVER, naverUserInfo.getResponse().getId(), naverAuthenticationToken.getOAuthAccessToken(), naverAuthenticationToken.getName()); + } + + @Override + public boolean supports(Class authentication) { + return NaverLoginAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/dev/moim/global/security/util/NaverLoginAuthenticationToken.java b/src/main/java/com/dev/moim/global/security/util/NaverLoginAuthenticationToken.java new file mode 100644 index 00000000..d81a8418 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/util/NaverLoginAuthenticationToken.java @@ -0,0 +1,38 @@ +package com.dev.moim.global.security.util; + +import com.dev.moim.domain.account.entity.enums.Provider; +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +@Getter +public class NaverLoginAuthenticationToken extends AbstractAuthenticationToken { + + private final Provider provider; + private final String providerId; + private final String oAuthAccessToken; + private final String email; + + public NaverLoginAuthenticationToken(Provider provider, String providerId, String oAuthAccessToken, String email) { + super(null); + this.provider = provider; + this.providerId = providerId; + this.oAuthAccessToken = oAuthAccessToken; + this.email = email; + setAuthenticated(false); + } + + @Override + public String getName() { + return email; + } + + @Override + public Provider getPrincipal() { + return provider; + } + + @Override + public String getCredentials() { + return providerId; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/security/util/OIDCAuthenticationProvider.java b/src/main/java/com/dev/moim/global/security/util/OIDCAuthenticationProvider.java new file mode 100644 index 00000000..6c1a81a2 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/util/OIDCAuthenticationProvider.java @@ -0,0 +1,37 @@ +package com.dev.moim.global.security.util; + +import com.dev.moim.domain.account.dto.OIDCDecodePayload; +import com.dev.moim.domain.account.entity.enums.Provider; +import com.dev.moim.global.security.service.OIDCService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Slf4j +@Component +public class OIDCAuthenticationProvider implements AuthenticationProvider { + + private final OIDCService oidcService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + OIDCAuthenticationToken oidcAuthenticationToken = (OIDCAuthenticationToken) authentication; + + Provider provider = oidcAuthenticationToken.getPrincipal(); + String idToken = oidcAuthenticationToken.getIdToken(); + + OIDCDecodePayload oidcDecodePayload = oidcService.getOIDCDecodePayload(provider, idToken); + + return new OIDCAuthenticationToken(provider, oidcDecodePayload.sub(), idToken, oidcDecodePayload.email()); + } + + @Override + public boolean supports(Class authentication) { + return OIDCAuthenticationToken.class.isAssignableFrom(authentication); + } +} + diff --git a/src/main/java/com/dev/moim/global/security/util/OIDCAuthenticationToken.java b/src/main/java/com/dev/moim/global/security/util/OIDCAuthenticationToken.java new file mode 100644 index 00000000..90990727 --- /dev/null +++ b/src/main/java/com/dev/moim/global/security/util/OIDCAuthenticationToken.java @@ -0,0 +1,39 @@ +package com.dev.moim.global.security.util; + +import com.dev.moim.domain.account.entity.enums.Provider; +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +@Getter +public class OIDCAuthenticationToken extends AbstractAuthenticationToken { + + private final Provider provider; + private final String providerId; + private final String idToken; + private final String email; + + public OIDCAuthenticationToken(Provider provider, String providerId, String idToken, String email) { + super(null); + this.provider = provider; + this.providerId = providerId; + this.idToken = idToken; + this.email = email; + setAuthenticated(false); + } + + @Override + public String getName() { + return email; + } + + @Override + public Provider getPrincipal() { + return provider; + } + + @Override + public String getCredentials() { + return providerId; + } +} + diff --git a/src/main/java/com/dev/moim/global/util/HttpRequestUtil.java b/src/main/java/com/dev/moim/global/util/HttpRequestUtil.java new file mode 100644 index 00000000..d80d4d51 --- /dev/null +++ b/src/main/java/com/dev/moim/global/util/HttpRequestUtil.java @@ -0,0 +1,34 @@ +package com.dev.moim.global.util; + +import com.dev.moim.global.error.handler.AuthException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class HttpRequestUtil { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static T readBody(HttpServletRequest request, Class requestDTO) { + try { + return objectMapper.readValue(request.getInputStream(), requestDTO); + } catch (IOException e) { + throw new AuthException(INVALID_REQUEST_BODY); + } + } + + public static String readHeader(HttpServletRequest request, String headerName) { + String headerValue = request.getHeader(headerName); + if (headerValue == null || headerValue.isEmpty()) { + throw new AuthException(INVALID_REQUEST_HEADER); + } + return headerValue; + } +} diff --git a/src/main/java/com/dev/moim/global/util/HttpResponseUtil.java b/src/main/java/com/dev/moim/global/util/HttpResponseUtil.java new file mode 100644 index 00000000..fa98a9f7 --- /dev/null +++ b/src/main/java/com/dev/moim/global/util/HttpResponseUtil.java @@ -0,0 +1,36 @@ +package com.dev.moim.global.util; + +import com.dev.moim.global.common.BaseResponse; +import com.dev.moim.global.common.code.status.SuccessStatus; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +import java.io.IOException; + +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class HttpResponseUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void setSuccessResponse(HttpServletResponse response, SuccessStatus status, Object body) + throws IOException { + String responseBody = objectMapper.writeValueAsString(BaseResponse.of(status, body)); + response.setContentType("application/json"); + response.setStatus(status.getHttpStatus().value()); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(responseBody); + } + + public static void setErrorResponse(HttpServletResponse response, HttpStatus httpStatus, Object body) + throws IOException { + response.setContentType("application/json"); + response.setStatus(httpStatus.value()); + response.setCharacterEncoding("UTF-8"); + objectMapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/src/main/java/com/dev/moim/global/util/TimeUtil.java b/src/main/java/com/dev/moim/global/util/TimeUtil.java new file mode 100644 index 00000000..d671a635 --- /dev/null +++ b/src/main/java/com/dev/moim/global/util/TimeUtil.java @@ -0,0 +1,14 @@ +package com.dev.moim.global.util; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Optional; + +public class TimeUtil { + public static LocalDateTime toLocalDateTime(Timestamp timestamp) { + return Optional.ofNullable(timestamp) + .map(ts -> ts.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()) + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/annotation/AddAssigneeValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/AddAssigneeValidation.java new file mode 100644 index 00000000..7fabc70f --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/AddAssigneeValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.AddAssigneeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = AddAssigneeValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AddAssigneeValidation { + String message() default "todo를 할당받을 멤버가 유효하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/CheckAdminValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/CheckAdminValidation.java new file mode 100644 index 00000000..e03acc8a --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/CheckAdminValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.CheckAdminValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckAdminValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckAdminValidation { + String message() default "해당 유저는 관리자가 아닙니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/CheckCursorValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/CheckCursorValidation.java new file mode 100644 index 00000000..9d5dc238 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/CheckCursorValidation.java @@ -0,0 +1,21 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.CheckCursorValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = CheckCursorValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckCursorValidation { + String message() default "허용 되지 않은 cursor 값 입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/CheckOwnerValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/CheckOwnerValidation.java new file mode 100644 index 00000000..48e375b0 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/CheckOwnerValidation.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.validation.annotation; + + +import com.dev.moim.global.validation.validator.CheckOwnerValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckOwnerValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckOwnerValidation { + String message() default "해당 유저는 owner가 아닙니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/CheckPageValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/CheckPageValidation.java new file mode 100644 index 00000000..8371f30f --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/CheckPageValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.CheckPageValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckPageValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckPageValidation { + String message() default "허용 되지 않은 page 값 입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/CheckSizeValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/CheckSizeValidation.java new file mode 100644 index 00000000..27c44025 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/CheckSizeValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.CheckSizeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CheckSizeValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckSizeValidation { + String message() default "허용 되지 않은 size 값 입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/CheckTakeValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/CheckTakeValidation.java new file mode 100644 index 00000000..531aa9d0 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/CheckTakeValidation.java @@ -0,0 +1,21 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.CheckTakeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = CheckTakeValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface CheckTakeValidation { + String message() default "허용 되지 않은 take 값 입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/DeletableProfileValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/DeletableProfileValidation.java new file mode 100644 index 00000000..0e7f5f7b --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/DeletableProfileValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.DeletableProfileValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = DeletableProfileValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DeletableProfileValidation { + String message() default "해당 프로필을 사용 중인 모임이 있습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/DeleteAssigneeValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/DeleteAssigneeValidation.java new file mode 100644 index 00000000..6a39cba5 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/DeleteAssigneeValidation.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.DeleteAssigneeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = DeleteAssigneeValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DeleteAssigneeValidation { + String message() default "todo를 할당받을 멤버가 유효하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} + diff --git a/src/main/java/com/dev/moim/global/validation/annotation/ExistEmailValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/ExistEmailValidation.java new file mode 100644 index 00000000..5b6b5560 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/ExistEmailValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.ExistEmailValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = ExistEmailValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistEmailValidation { + String message() default "해당 메일로 가입된 계정이 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/ExistUserValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/ExistUserValidation.java new file mode 100644 index 00000000..2809281b --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/ExistUserValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.ExistUserValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = ExistUserValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistUserValidation { + String message() default "해당 유저를 찾을 수 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/FcmTokenValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/FcmTokenValidation.java new file mode 100644 index 00000000..10bb4c13 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/FcmTokenValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.FcmTokenValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = FcmTokenValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface FcmTokenValidation { + String message() default "FCM 토큰이 유효하지 않습니다.."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/IndividualPlanValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/IndividualPlanValidation.java new file mode 100644 index 00000000..a9fbf926 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/IndividualPlanValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.IndividualPlanValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = IndividualPlanValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface IndividualPlanValidation { + String message() default "해당 일정의 작성자가 아닙니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/JoinPasswordValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/JoinPasswordValidation.java new file mode 100644 index 00000000..15309888 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/JoinPasswordValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.JoinPasswordValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = JoinPasswordValidator.class) +@Target( {ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JoinPasswordValidation { + String message() default "비밀번호 조건에 맞지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/LocalAccountValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/LocalAccountValidation.java new file mode 100644 index 00000000..ddcd138b --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/LocalAccountValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.LocalAccountValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = LocalAccountValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface LocalAccountValidation { + String message() default "이미 가입된 메일입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/MoimValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/MoimValidation.java new file mode 100644 index 00000000..dd56949c --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/MoimValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.MoimValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = MoimValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MoimValidation { + String message() default "존재하지 않는 모임입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/annotation/OAuthAccountValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/OAuthAccountValidation.java new file mode 100644 index 00000000..2c93234e --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/OAuthAccountValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.OAuthAccountValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = OAuthAccountValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface OAuthAccountValidation { + String message() default "이미 가입된 소셜 계정입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/PlanAuthorityValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/PlanAuthorityValidation.java new file mode 100644 index 00000000..3838e5f0 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/PlanAuthorityValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.PlanAuthorityValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PlanAuthorityValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PlanAuthorityValidation { + String message() default "일정 수정, 삭제 권한이 있는 유저가 아닙니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/PlanValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/PlanValidation.java new file mode 100644 index 00000000..df51b58a --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/PlanValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.PlanValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = PlanValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PlanValidation { + String message() default "존재하지 않는 일정입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/ProfileOwnerValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/ProfileOwnerValidation.java new file mode 100644 index 00000000..23b245a1 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/ProfileOwnerValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.ProfileOwnerValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = ProfileOwnerValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ProfileOwnerValidation { + String message() default "해당 유저의 프로필이 아닙니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/QuitValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/QuitValidation.java new file mode 100644 index 00000000..3719340e --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/QuitValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.QuitValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = QuitValidator.class) +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface QuitValidation { + String message() default "모임장 권한이 있는 모임이 있습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/annotation/SelfReviewValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/SelfReviewValidation.java new file mode 100644 index 00000000..4871a3f9 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/SelfReviewValidation.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.SelfReviewValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = SelfReviewValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SelfReviewValidation { + String message() default "유저 본인에게 리뷰를 남길 수 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} + diff --git a/src/main/java/com/dev/moim/global/validation/annotation/TodoAssigneeStatusValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/TodoAssigneeStatusValidation.java new file mode 100644 index 00000000..8880cda4 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/TodoAssigneeStatusValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.TodoAssigneeStatusValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = TodoAssigneeStatusValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TodoAssigneeStatusValidation { + String message() default "todo status가 유효하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/TodoAssigneeValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/TodoAssigneeValidation.java new file mode 100644 index 00000000..6c856b8c --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/TodoAssigneeValidation.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.TodoAssigneeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = TodoAssigneeValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TodoAssigneeValidation { + String message() default "해당 유저는 해당 todo를 조회할 권한이 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} + diff --git a/src/main/java/com/dev/moim/global/validation/annotation/TodoTargetUserValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/TodoTargetUserValidation.java new file mode 100644 index 00000000..b31963af --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/TodoTargetUserValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.TodoTargetUserValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = TodoTargetUserValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TodoTargetUserValidation { + String message() default "todo를 할당받을 멤버가 유효하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/TodoUpdateDueDateValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/TodoUpdateDueDateValidation.java new file mode 100644 index 00000000..e7124345 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/TodoUpdateDueDateValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.TodoUpdateDueDateValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = TodoUpdateDueDateValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TodoUpdateDueDateValidation { + String message() default "todo 마감 기한을 현재 날짜 이전으로 수정할 수 없습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/TodoValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/TodoValidation.java new file mode 100644 index 00000000..3a114a75 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/TodoValidation.java @@ -0,0 +1,18 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.TodoValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = TodoValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TodoValidation { + String message() default "존재하지 않는 todo입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} + diff --git a/src/main/java/com/dev/moim/global/validation/annotation/UpdatePasswordValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/UpdatePasswordValidation.java new file mode 100644 index 00000000..af18735a --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/UpdatePasswordValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.UpdatePasswordValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = UpdatePasswordValidator.class) +@Target( {ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UpdatePasswordValidation { + String message() default "비밀번호 조건에 맞지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/UserMoimListValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/UserMoimListValidation.java new file mode 100644 index 00000000..03dc2190 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/UserMoimListValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.UserMoimListValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = UserMoimListValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserMoimListValidation { + String message() default "유저가 참여하지 않는 모임이 포함되어 있습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/UserMoimValidaton.java b/src/main/java/com/dev/moim/global/validation/annotation/UserMoimValidaton.java new file mode 100644 index 00000000..b294941a --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/UserMoimValidaton.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.UserMoimValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = UserMoimValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserMoimValidaton { + String message() default "해당 유저는 모임의 멤버가 아닙니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/UserPlanDuplicateValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/UserPlanDuplicateValidation.java new file mode 100644 index 00000000..7fe08d66 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/UserPlanDuplicateValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.UserPlanDuplicateValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = UserPlanDuplicateValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserPlanDuplicateValidation { + String message() default "이미 해당 모임 일정에 참여 신청했습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/annotation/UserPlanValidation.java b/src/main/java/com/dev/moim/global/validation/annotation/UserPlanValidation.java new file mode 100644 index 00000000..9705b65a --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/annotation/UserPlanValidation.java @@ -0,0 +1,17 @@ +package com.dev.moim.global.validation.annotation; + +import com.dev.moim.global.validation.validator.UserPlanValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = UserPlanValidator.class) +@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserPlanValidation { + String message() default "유저가 참여하지 않는 일정입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/AddAssigneeValidator.java b/src/main/java/com/dev/moim/global/validation/validator/AddAssigneeValidator.java new file mode 100644 index 00000000..df503b87 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/AddAssigneeValidator.java @@ -0,0 +1,62 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.dto.todo.AddTodoAssigneeDTO; +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.validation.annotation.AddAssigneeValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AddAssigneeValidator implements ConstraintValidator { + + private final TodoQueryService todoQueryService; + private final MoimQueryService moimQueryService; + + @Override + public void initialize(AddAssigneeValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(AddTodoAssigneeDTO request, ConstraintValidatorContext context) { + + List addAssigneeIdList = request.addAssigneeIdList(); + List moimMemberIdList = moimQueryService.findAllMemberIdByMoimId(request.moimId()); + + boolean hasNonMember = addAssigneeIdList.stream() + .anyMatch(id -> !moimMemberIdList.contains(id)); + if (hasNonMember) { + addConstraintViolation(context, INVALID_MOIM_MEMBER.getMessage()); + return false; + } + + List assigneeIdList = todoQueryService.findAssigneeByTodoId(request.todoId()).stream() + .map(userTodo -> userTodo.getUser().getId()) + .toList(); + + boolean hasDuplicate = addAssigneeIdList.stream() + .anyMatch(assigneeIdList::contains); + if (hasDuplicate) { + addConstraintViolation(context, IS_ALREADY_TODO_ASSIGNEE.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode("addAssigneeIdList") + .addConstraintViolation(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/validator/CheckAdminValidator.java b/src/main/java/com/dev/moim/global/validation/validator/CheckAdminValidator.java new file mode 100644 index 00000000..fd3b1f1f --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/CheckAdminValidator.java @@ -0,0 +1,53 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import com.dev.moim.global.validation.annotation.CheckAdminValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_MOIM_MEMBER; +import static com.dev.moim.global.common.code.status.ErrorStatus.MOIM_NOT_ADMIN; + +@RequiredArgsConstructor +@Component +public class CheckAdminValidator implements ConstraintValidator { + + private final UserMoimRepository userMoimRepository; + + @Override + public void initialize(CheckAdminValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long moimId, ConstraintValidatorContext context) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + Optional userMoim = userMoimRepository.findByUserIdAndMoimId(Long.valueOf(authentication.getName()), moimId, JoinStatus.COMPLETE); + + if (userMoim.isEmpty()) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(INVALID_MOIM_MEMBER.getMessage()) + .addConstraintViolation(); + return false; + } else { + if (userMoim.get().getMoimRole().equals(MoimRole.ADMIN) || userMoim.get().getMoimRole().equals(MoimRole.OWNER)) { + return true; + } else { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(MOIM_NOT_ADMIN.getMessage()) + .addConstraintViolation(); + return false; + } + } + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/CheckCursorValidator.java b/src/main/java/com/dev/moim/global/validation/validator/CheckCursorValidator.java new file mode 100644 index 00000000..070d8637 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/CheckCursorValidator.java @@ -0,0 +1,29 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.validation.annotation.CheckCursorValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CheckCursorValidator implements ConstraintValidator { + @Override + public void initialize(CheckCursorValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long value, ConstraintValidatorContext context) { + if (value <= 0) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(ErrorStatus.NOT_VALID_CURSOR.getMessage()) + .addConstraintViolation(); + + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/CheckOwnerValidator.java b/src/main/java/com/dev/moim/global/validation/validator/CheckOwnerValidator.java new file mode 100644 index 00000000..564c2c8b --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/CheckOwnerValidator.java @@ -0,0 +1,57 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.entity.UserMoim; +import com.dev.moim.domain.moim.entity.enums.JoinStatus; +import com.dev.moim.domain.moim.entity.enums.MoimRole; +import com.dev.moim.domain.moim.repository.UserMoimRepository; +import com.dev.moim.global.validation.annotation.CheckOwnerValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_MOIM_MEMBER; +import static com.dev.moim.global.common.code.status.ErrorStatus.MOIM_NOT_ADMIN; + +@RequiredArgsConstructor +@Component +public class CheckOwnerValidator implements ConstraintValidator { + + private final UserMoimRepository userMoimRepository; + + @Override + public void initialize(CheckOwnerValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long moimId, ConstraintValidatorContext context) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + Optional userMoim = userMoimRepository.findByUserIdAndMoimId(Long.valueOf(authentication.getName()), moimId, JoinStatus.COMPLETE); + + if (userMoim.isEmpty()) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(INVALID_MOIM_MEMBER.getMessage()) + .addPropertyNode("userId") + .addPropertyNode("moimId") + .addConstraintViolation(); + return false; + } else { + if (userMoim.get().getMoimRole().equals(MoimRole.OWNER)) { + return true; + } else { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(MOIM_NOT_ADMIN.getMessage()) + .addPropertyNode("userId") + .addPropertyNode("moimId") + .addConstraintViolation(); + return false; + } + } + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/CheckPageValidator.java b/src/main/java/com/dev/moim/global/validation/validator/CheckPageValidator.java new file mode 100644 index 00000000..de085708 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/CheckPageValidator.java @@ -0,0 +1,30 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.validation.annotation.CheckPageValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.NOT_VALID_PAGE; + +@Component +@RequiredArgsConstructor +public class CheckPageValidator implements ConstraintValidator { + @Override + public void initialize(CheckPageValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value <= 0) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(NOT_VALID_PAGE.getMessage()) + .addConstraintViolation(); + + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/CheckSizeValidator.java b/src/main/java/com/dev/moim/global/validation/validator/CheckSizeValidator.java new file mode 100644 index 00000000..7e79d102 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/CheckSizeValidator.java @@ -0,0 +1,30 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.validation.annotation.CheckSizeValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.NOT_VALID_SIZE; + +@Component +@RequiredArgsConstructor +public class CheckSizeValidator implements ConstraintValidator { + @Override + public void initialize(CheckSizeValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value <= 0) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(NOT_VALID_SIZE.getMessage()) + .addConstraintViolation(); + + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/CheckTakeValidator.java b/src/main/java/com/dev/moim/global/validation/validator/CheckTakeValidator.java new file mode 100644 index 00000000..b5b61198 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/CheckTakeValidator.java @@ -0,0 +1,29 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.common.code.status.ErrorStatus; +import com.dev.moim.global.validation.annotation.CheckTakeValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CheckTakeValidator implements ConstraintValidator { + @Override + public void initialize(CheckTakeValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + if (value <= 0) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(ErrorStatus.NOT_VALID_TAKE.getMessage()) + .addConstraintViolation(); + + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/DeletableProfileValidator.java b/src/main/java/com/dev/moim/global/validation/validator/DeletableProfileValidator.java new file mode 100644 index 00000000..949af713 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/DeletableProfileValidator.java @@ -0,0 +1,63 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.account.entity.enums.ProfileType; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.DeletableProfileValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@RequiredArgsConstructor +@Component +public class DeletableProfileValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(DeletableProfileValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long profileId, ConstraintValidatorContext context) { + + Optional userProfile = userQueryService.findUserProfile(profileId); + if (userProfile.isEmpty()) { + addConstraintViolation(context, USER_PROFILE_NOT_FOUND.getMessage()); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (!userProfile.get().getUser().getId().equals(Long.valueOf(authentication.getName()))) { + addConstraintViolation(context, NOT_USER_PROFILE_OWNER.getMessage()); + return false; + } + + if (userProfile.get().getProfileType().equals(ProfileType.MAIN)) { + addConstraintViolation(context, CANNOT_DELETE_MAIN_USER_PROFILE.getMessage()); + return false; + } + + boolean isUsedProfile = userQueryService.existsByUserProfileIdAndJoinStatus(profileId); + if (isUsedProfile) { + addConstraintViolation(context, USER_PROFILE_IN_USE.getMessage()); + return false; + } + + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/DeleteAssigneeValidator.java b/src/main/java/com/dev/moim/global/validation/validator/DeleteAssigneeValidator.java new file mode 100644 index 00000000..6218a145 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/DeleteAssigneeValidator.java @@ -0,0 +1,62 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.dto.todo.DeleteTodoAssigneeDTO; +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.validation.annotation.DeleteAssigneeValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DeleteAssigneeValidator implements ConstraintValidator { + + private final TodoQueryService todoQueryService; + private final MoimQueryService moimQueryService; + + @Override + public void initialize(DeleteAssigneeValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(DeleteTodoAssigneeDTO request, ConstraintValidatorContext context) { + + List deleteAssigneeIdList = request.deleteAssigneeIdList(); + List moimMemberIdList = moimQueryService.findAllMemberIdByMoimId(request.moimId()); + + boolean hasNonMember = deleteAssigneeIdList.stream() + .anyMatch(id -> !moimMemberIdList.contains(id)); + if (hasNonMember) { + addConstraintViolation(context, INVALID_MOIM_MEMBER.getMessage()); + return false; + } + + List assigneeIdList = todoQueryService.findAssigneeByTodoId(request.todoId()).stream() + .map(userTodo -> userTodo.getUser().getId()) + .toList(); + + boolean hasNonAssignee = deleteAssigneeIdList.stream() + .anyMatch(id -> !assigneeIdList.contains(id)); + if (hasNonAssignee) { + addConstraintViolation(context, NOT_TODO_ASSIGNEE.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode("deleteAssigneeIdList") + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/ExistEmailValidator.java b/src/main/java/com/dev/moim/global/validation/validator/ExistEmailValidator.java new file mode 100644 index 00000000..e2be9bfb --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/ExistEmailValidator.java @@ -0,0 +1,35 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.ExistEmailValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_NOT_FOUND; + +@Component +@RequiredArgsConstructor +public class ExistEmailValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(ExistEmailValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(String email, ConstraintValidatorContext context) { + boolean isExistEmail = userQueryService.isExistEmail(email); + + if (!isExistEmail){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(USER_NOT_FOUND.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/ExistUserValidator.java b/src/main/java/com/dev/moim/global/validation/validator/ExistUserValidator.java new file mode 100644 index 00000000..a853b594 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/ExistUserValidator.java @@ -0,0 +1,38 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.ExistUserValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_NOT_FOUND; + +@Component +@RequiredArgsConstructor +public class ExistUserValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(ExistUserValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long userId, ConstraintValidatorContext context) { + Optional user = userQueryService.findUserById(userId); + + if (user.isEmpty()){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(USER_NOT_FOUND.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/FcmTokenValidator.java b/src/main/java/com/dev/moim/global/validation/validator/FcmTokenValidator.java new file mode 100644 index 00000000..08e61369 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/FcmTokenValidator.java @@ -0,0 +1,47 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.error.handler.AuthException; +import com.dev.moim.global.firebase.service.FcmQueryService; +import com.dev.moim.global.validation.annotation.FcmTokenValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.FCM_NOT_VALID; +import static com.dev.moim.global.common.code.status.ErrorStatus.FCM_TOKEN_REQUIRED; + +@Component +@RequiredArgsConstructor +public class FcmTokenValidator implements ConstraintValidator { + + private final FcmQueryService fcmQueryService; + + @Override + public void initialize(FcmTokenValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(String fcmToken, ConstraintValidatorContext context) { + + if (fcmToken == null || fcmToken.trim().isEmpty()) { + addConstraintViolation(context, FCM_TOKEN_REQUIRED.getMessage()); + return false; + } + + try { + fcmQueryService.isTokenValid("MOIM", fcmToken); + } catch (AuthException e) { + addConstraintViolation(context, FCM_NOT_VALID.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String errorStatus) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(errorStatus) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/IndividualPlanValidator.java b/src/main/java/com/dev/moim/global/validation/validator/IndividualPlanValidator.java new file mode 100644 index 00000000..dec8c9fb --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/IndividualPlanValidator.java @@ -0,0 +1,55 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.entity.IndividualPlan; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.IndividualPlanValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Objects; +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INDIVIDUAL_PLAN_NOT_FOUND; +import static com.dev.moim.global.common.code.status.ErrorStatus.NOT_INDIVIDUAL_PLAN_OWNER; + +@Slf4j +@Component +@RequiredArgsConstructor +public class IndividualPlanValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(IndividualPlanValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long individualPlanId, ConstraintValidatorContext context) { + + Optional individualPlan = userQueryService.findUserByPlanId(individualPlanId); + if (individualPlan.isEmpty()) { + addConstraintViolation(context, INDIVIDUAL_PLAN_NOT_FOUND.getMessage()); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (!Objects.equals(individualPlan.get().getUser().getId(), Long.valueOf(authentication.getName()))) { + addConstraintViolation(context, NOT_INDIVIDUAL_PLAN_OWNER.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String errorStatus) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(errorStatus) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/JoinPasswordValidator.java b/src/main/java/com/dev/moim/global/validation/validator/JoinPasswordValidator.java new file mode 100644 index 00000000..3332c1b9 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/JoinPasswordValidator.java @@ -0,0 +1,43 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.dto.JoinRequest; +import com.dev.moim.global.validation.annotation.JoinPasswordValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.domain.account.entity.enums.Provider.LOCAL; +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_PASSWORD; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JoinPasswordValidator implements ConstraintValidator { + + @Override + public void initialize(JoinPasswordValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(JoinRequest request, ConstraintValidatorContext context) { + if (request.provider() == LOCAL && !isPasswordValid(request.password(), context)) { + return false; + } + return true; + } + + private boolean isPasswordValid(String password, ConstraintValidatorContext context) { + String passwordPattern = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[\\W_]).{8,16}$"; + + if (password == null || !password.matches(passwordPattern)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(INVALID_PASSWORD.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/LocalAccountValidator.java b/src/main/java/com/dev/moim/global/validation/validator/LocalAccountValidator.java new file mode 100644 index 00000000..685ad82c --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/LocalAccountValidator.java @@ -0,0 +1,43 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.dto.JoinRequest; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.LocalAccountValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.domain.account.entity.enums.Provider.LOCAL; +import static com.dev.moim.global.common.code.status.ErrorStatus.EMAIL_DUPLICATION; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LocalAccountValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(LocalAccountValidation constraintAnnotation) {} + + @Override + public boolean isValid(JoinRequest request, ConstraintValidatorContext context) { + if (request.provider() == LOCAL) { + return validateEmailDuplication(request.email(), context); + } + return true; + } + + private boolean validateEmailDuplication(String email, ConstraintValidatorContext context) { + if (userQueryService.existsByEmail(email)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(EMAIL_DUPLICATION.getMessage()) + .addConstraintViolation(); + + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/validator/MoimValidator.java b/src/main/java/com/dev/moim/global/validation/validator/MoimValidator.java new file mode 100644 index 00000000..78e49e34 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/MoimValidator.java @@ -0,0 +1,38 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.global.validation.annotation.MoimValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.MOIM_NOT_FOUND; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MoimValidator implements ConstraintValidator { + + private final MoimQueryService moimQueryService; + + @Override + public void initialize(MoimValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long moimId, ConstraintValidatorContext context) { + + boolean isValidMoim = moimQueryService.existsByMoimId(moimId); + + if (!isValidMoim) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(MOIM_NOT_FOUND.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/OAuthAccountValidator.java b/src/main/java/com/dev/moim/global/validation/validator/OAuthAccountValidator.java new file mode 100644 index 00000000..3fcbfd82 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/OAuthAccountValidator.java @@ -0,0 +1,53 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.dto.JoinRequest; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.OAuthAccountValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.domain.account.entity.enums.Provider.LOCAL; +import static com.dev.moim.global.common.code.status.ErrorStatus.OAUTH_ACCOUNT_DUPLICATION; +import static com.dev.moim.global.common.code.status.ErrorStatus.PROVIDER_ID_NOT_FOUND; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuthAccountValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(OAuthAccountValidation constraintAnnotation) {} + + @Override + public boolean isValid(JoinRequest request, ConstraintValidatorContext context) { + + if (request.provider() == LOCAL) { + return true; + } + + String providerId = request.providerId(); + if (providerId == null || providerId.isEmpty()) { + addConstraintViolation(context, PROVIDER_ID_NOT_FOUND.getMessage()); + return false; + } + + boolean isDuplicated = userQueryService.existsByProviderAndProviderId(request.provider(), request.providerId()); + if (isDuplicated) { + addConstraintViolation(context, OAUTH_ACCOUNT_DUPLICATION.getMessage()); + return false; + } + + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/PlanAuthorityValidator.java b/src/main/java/com/dev/moim/global/validation/validator/PlanAuthorityValidator.java new file mode 100644 index 00000000..c42cab20 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/PlanAuthorityValidator.java @@ -0,0 +1,59 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.global.error.handler.MoimException; +import com.dev.moim.global.error.handler.PlanException; +import com.dev.moim.global.validation.annotation.PlanAuthorityValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PlanAuthorityValidator implements ConstraintValidator { + + private final CalenderQueryService calenderQueryService; + private final MoimQueryService moimQueryService; + + @Override + public void initialize(PlanAuthorityValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long planId, ConstraintValidatorContext context) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + try { + Long userId = Long.valueOf(authentication.getName()); + Long writerId = calenderQueryService.findPlanWriter(planId); + Long ownerId = moimQueryService.findMoimOwner(planId); + + if (userId.equals(writerId) || userId.equals(ownerId)) { + return true; + } + addConstraintViolation(context, PLAN_EDIT_UNAUTHORIZED.getMessage()); + return false; + } catch(PlanException e) { + addConstraintViolation(context, PLAN_NOT_FOUND.getMessage()); + return false; + } catch (MoimException e) { + addConstraintViolation(context, MOIM_OWNER_NOT_FOUND.getMessage()); + return false; + } + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/PlanValidator.java b/src/main/java/com/dev/moim/global/validation/validator/PlanValidator.java new file mode 100644 index 00000000..22899c32 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/PlanValidator.java @@ -0,0 +1,38 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.global.validation.annotation.PlanValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.PLAN_NOT_FOUND; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PlanValidator implements ConstraintValidator { + + private final CalenderQueryService calenderQueryService; + + @Override + public void initialize(PlanValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long planId, ConstraintValidatorContext context) { + + boolean isValidPlan = calenderQueryService.existsByPlanId(planId); + + if (!isValidPlan) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(PLAN_NOT_FOUND.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/ProfileOwnerValidator.java b/src/main/java/com/dev/moim/global/validation/validator/ProfileOwnerValidator.java new file mode 100644 index 00000000..9ec932b0 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/ProfileOwnerValidator.java @@ -0,0 +1,55 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.entity.UserProfile; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.ProfileOwnerValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ProfileOwnerValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(ProfileOwnerValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long profileId, ConstraintValidatorContext context) { + + Optional userProfile = userQueryService.findUserProfile(profileId); + + if (userProfile.isEmpty()) { + addConstraintViolation(context, USER_PROFILE_NOT_FOUND.getMessage()); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (!userProfile.get().getUser().getId().equals(Long.valueOf(authentication.getName()))) { + addConstraintViolation(context, NOT_USER_PROFILE_OWNER.getMessage()); + return false; + } + + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/QuitValidator.java b/src/main/java/com/dev/moim/global/validation/validator/QuitValidator.java new file mode 100644 index 00000000..907da012 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/QuitValidator.java @@ -0,0 +1,39 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.QuitValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.IS_MOIM_OWNER; + +@Slf4j +@Component +@RequiredArgsConstructor +public class QuitValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(QuitValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(User user, ConstraintValidatorContext context) { + + boolean isMoimOwner = userQueryService.isMoimOwner(user); + + if (isMoimOwner) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(IS_MOIM_OWNER.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/SelfReviewValidator.java b/src/main/java/com/dev/moim/global/validation/validator/SelfReviewValidator.java new file mode 100644 index 00000000..cc95cf73 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/SelfReviewValidator.java @@ -0,0 +1,54 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.account.entity.User; +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.error.handler.UserException; +import com.dev.moim.global.validation.annotation.SelfReviewValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SelfReviewValidator implements ConstraintValidator { + + private final UserQueryService userQueryService; + + @Override + public void initialize(SelfReviewValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long targetUserId, ConstraintValidatorContext context) { + + try { + User targetUser = userQueryService.findUserById(targetUserId) + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (targetUser.getId().toString().equals(authentication.getName())) { + addConstraintViolation(context, SELF_REVIEW_FORBIDDEN.getMessage()); + return false; + } + return true; + } catch (UserException e) { + addConstraintViolation(context, USER_NOT_FOUND.getMessage()); + return false; + } + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/validator/TodoAssigneeStatusValidator.java b/src/main/java/com/dev/moim/global/validation/validator/TodoAssigneeStatusValidator.java new file mode 100644 index 00000000..c6e770f3 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/TodoAssigneeStatusValidator.java @@ -0,0 +1,73 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.dto.todo.UpdateTodoStatusDTO; +import com.dev.moim.domain.moim.entity.Todo; +import com.dev.moim.domain.moim.entity.UserTodo; +import com.dev.moim.domain.moim.entity.enums.TodoAssigneeStatus; +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.validation.annotation.TodoAssigneeStatusValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@RequiredArgsConstructor +@Component +public class TodoAssigneeStatusValidator implements ConstraintValidator { + + private final TodoQueryService todoQueryService; + + @Override + public void initialize(TodoAssigneeStatusValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(UpdateTodoStatusDTO request, ConstraintValidatorContext context) { + + Optional todo = todoQueryService.findTodoByTodoId(request.todoId()); + if (todo.isEmpty()) { + addConstraintViolation(context, TODO_NOT_FOUND.getMessage(), "todoId"); + return false; + } + + if (todo.get().getDueDate().isBefore(LocalDateTime.now())) { + addConstraintViolation(context, TODO_DUE_DATE_EXPIRED.getMessage(), "todoId"); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Optional userTodo = todoQueryService.findByUserIdAndTodoId(Long.valueOf(authentication.getName()), request.todoId()); + if (userTodo.isEmpty()) { + addConstraintViolation(context, NOT_TODO_ASSIGNEE.getMessage(), "userId"); + return false; + } + + if (request.todoAssigneeStatus() == TodoAssigneeStatus.OVERDUE) { + addConstraintViolation(context, TODO_INVALID_STATE_REQUEST.getMessage(), "todoStatus"); + return false; + } + else if (request.todoAssigneeStatus() == userTodo.get().getStatus()) { + addConstraintViolation(context, TODO_STATUS_SAME.getMessage(), "todoStatus"); + return false; + } + + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message, String propertyNode) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode(propertyNode) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/TodoAssigneeValidator.java b/src/main/java/com/dev/moim/global/validation/validator/TodoAssigneeValidator.java new file mode 100644 index 00000000..175293ce --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/TodoAssigneeValidator.java @@ -0,0 +1,51 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.validation.annotation.TodoAssigneeValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.NOT_TODO_ASSIGNEE; +import static com.dev.moim.global.common.code.status.ErrorStatus.TODO_NOT_FOUND; + +@RequiredArgsConstructor +@Component +public class TodoAssigneeValidator implements ConstraintValidator { + + private final TodoQueryService todoQueryService; + + @Override + public void initialize(TodoAssigneeValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long todoId, ConstraintValidatorContext context) { + + boolean isExistTodo = todoQueryService.existsByTodoId(todoId); + if (!isExistTodo) { + addConstraintViolation(context, TODO_NOT_FOUND.getMessage()); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + boolean isAssignee = todoQueryService.existsByUserIdAndTodoId(Long.valueOf(authentication.getName()), todoId); + + if (!isAssignee) { + addConstraintViolation(context, NOT_TODO_ASSIGNEE.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} + diff --git a/src/main/java/com/dev/moim/global/validation/validator/TodoTargetUserValidator.java b/src/main/java/com/dev/moim/global/validation/validator/TodoTargetUserValidator.java new file mode 100644 index 00000000..15c43015 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/TodoTargetUserValidator.java @@ -0,0 +1,60 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.dto.todo.CreateTodoDTO; +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.global.validation.annotation.TodoTargetUserValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.List; + +import static com.dev.moim.global.common.code.status.ErrorStatus.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TodoTargetUserValidator implements ConstraintValidator { + + private final MoimQueryService moimQueryService; + + @Override + public void initialize(TodoTargetUserValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(CreateTodoDTO request, ConstraintValidatorContext context) { + boolean isAssigneeSelectAll = request.isAssigneeSelectAll(); + List targetUserIdList = request.targetUserIdList(); + + if (isAssigneeSelectAll && !(targetUserIdList == null || targetUserIdList.isEmpty())) { + addConstraintViolation(context, TODO_ASSIGNEE_NOT_MATCH.getMessage()); + return false; + } + + if (!isAssigneeSelectAll && (targetUserIdList == null || targetUserIdList.isEmpty())) { + addConstraintViolation(context, TODO_ASSIGNEE_NULL.getMessage()); + return false; + } + + if (!isAssigneeSelectAll) { + List userMoimIdList = moimQueryService.findAllMemberIdByMoimId(request.moimId()); + if (!new HashSet<>(userMoimIdList).containsAll(targetUserIdList)) { + addConstraintViolation(context, INVALID_MOIM_MEMBER.getMessage()); + return false; + } + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addPropertyNode("targetUserIdList") + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/TodoUpdateDueDateValidator.java b/src/main/java/com/dev/moim/global/validation/validator/TodoUpdateDueDateValidator.java new file mode 100644 index 00000000..65ec5bc8 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/TodoUpdateDueDateValidator.java @@ -0,0 +1,35 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.validation.annotation.TodoUpdateDueDateValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_TODO_DUE_DATE; + +@Slf4j +@RequiredArgsConstructor +@Component +public class TodoUpdateDueDateValidator implements ConstraintValidator { + + @Override + public void initialize(TodoUpdateDueDateValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(LocalDate dueDate, ConstraintValidatorContext context) { + + if (dueDate.isBefore(LocalDate.now())) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(INVALID_TODO_DUE_DATE.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/TodoValidator.java b/src/main/java/com/dev/moim/global/validation/validator/TodoValidator.java new file mode 100644 index 00000000..78dd57ca --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/TodoValidator.java @@ -0,0 +1,35 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.TodoQueryService; +import com.dev.moim.global.validation.annotation.TodoValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.TODO_NOT_FOUND; + +@RequiredArgsConstructor +@Component +public class TodoValidator implements ConstraintValidator { + + private final TodoQueryService todoQueryService; + + @Override + public void initialize(TodoValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long todoId, ConstraintValidatorContext context) { + boolean isExistTodo = todoQueryService.existsByTodoId(todoId); + + if (!isExistTodo) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(TODO_NOT_FOUND.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/dev/moim/global/validation/validator/UpdatePasswordValidator.java b/src/main/java/com/dev/moim/global/validation/validator/UpdatePasswordValidator.java new file mode 100644 index 00000000..94f7d7d8 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/UpdatePasswordValidator.java @@ -0,0 +1,34 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.global.validation.annotation.UpdatePasswordValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_PASSWORD; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UpdatePasswordValidator implements ConstraintValidator { + + @Override + public void initialize(UpdatePasswordValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + String passwordPattern = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[\\W_]).{8,16}$"; + + if (password == null || !password.matches(passwordPattern)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(INVALID_PASSWORD.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/UserMoimListValidator.java b/src/main/java/com/dev/moim/global/validation/validator/UserMoimListValidator.java new file mode 100644 index 00000000..17d1e088 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/UserMoimListValidator.java @@ -0,0 +1,51 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.user.service.UserQueryService; +import com.dev.moim.global.validation.annotation.UserMoimListValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.List; + +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_MOIM_MEMBER; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserMoimListValidator implements ConstraintValidator> { + + private final UserQueryService userQueryService; + + @Override + public void initialize(UserMoimListValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(List moimIdList, ConstraintValidatorContext context) { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (moimIdList == null || moimIdList.isEmpty()) { + return true; + } + + List userMoimIdList = userQueryService.findUserMoimIdListByUserId(Long.valueOf(authentication.getName())); + + boolean isValidMember = new HashSet<>(userMoimIdList).containsAll(moimIdList); + + if (!isValidMember) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(INVALID_MOIM_MEMBER.getMessage()) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/UserMoimValidator.java b/src/main/java/com/dev/moim/global/validation/validator/UserMoimValidator.java new file mode 100644 index 00000000..7e5c2df0 --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/UserMoimValidator.java @@ -0,0 +1,52 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.MoimQueryService; +import com.dev.moim.global.validation.annotation.UserMoimValidaton; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import static com.dev.moim.domain.moim.entity.enums.JoinStatus.COMPLETE; +import static com.dev.moim.global.common.code.status.ErrorStatus.INVALID_MOIM_MEMBER; +import static com.dev.moim.global.common.code.status.ErrorStatus.MOIM_NOT_FOUND; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserMoimValidator implements ConstraintValidator { + + private final MoimQueryService moimQueryService; + + @Override + public void initialize(UserMoimValidaton constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long moimId, ConstraintValidatorContext context) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + boolean isValidMoim = moimQueryService.existsByMoimId(moimId); + if (!isValidMoim) { + addConstraintViolation(context, MOIM_NOT_FOUND.getMessage()); + return false; + } + + boolean isValidMember = moimQueryService.existsByUserIdAndMoimIdAndJoinStatus(Long.valueOf(authentication.getName()), moimId, COMPLETE); + if (!isValidMember) { + addConstraintViolation(context, INVALID_MOIM_MEMBER.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/UserPlanDuplicateValidator.java b/src/main/java/com/dev/moim/global/validation/validator/UserPlanDuplicateValidator.java new file mode 100644 index 00000000..a5a788cb --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/UserPlanDuplicateValidator.java @@ -0,0 +1,53 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.global.validation.annotation.UserPlanDuplicateValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.ALREADY_PARTICIPATE_IN_PLAN; +import static com.dev.moim.global.common.code.status.ErrorStatus.PLAN_NOT_FOUND; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserPlanDuplicateValidator implements ConstraintValidator { + + private final CalenderQueryService calenderQueryService; + + @Override + public void initialize(UserPlanDuplicateValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long planId, ConstraintValidatorContext context) { + + boolean isValidPlan = calenderQueryService.existsByPlanId(planId); + if (!isValidPlan) { + addConstraintViolation(context, PLAN_NOT_FOUND.getMessage()); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + boolean alreadyParticipate = calenderQueryService.existsByUserIdAndPlanId( + Long.valueOf(authentication.getName()), planId); + if (alreadyParticipate) { + addConstraintViolation(context, ALREADY_PARTICIPATE_IN_PLAN.getMessage()); + return false; + } + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/java/com/dev/moim/global/validation/validator/UserPlanValidator.java b/src/main/java/com/dev/moim/global/validation/validator/UserPlanValidator.java new file mode 100644 index 00000000..6ab88c6a --- /dev/null +++ b/src/main/java/com/dev/moim/global/validation/validator/UserPlanValidator.java @@ -0,0 +1,53 @@ +package com.dev.moim.global.validation.validator; + +import com.dev.moim.domain.moim.service.CalenderQueryService; +import com.dev.moim.global.validation.annotation.UserPlanValidation; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import static com.dev.moim.global.common.code.status.ErrorStatus.PLAN_NOT_FOUND; +import static com.dev.moim.global.common.code.status.ErrorStatus.USER_NOT_PART_OF_PLAN; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserPlanValidator implements ConstraintValidator { + + private final CalenderQueryService calenderQueryService; + + @Override + public void initialize(UserPlanValidation constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long planId, ConstraintValidatorContext context) { + + boolean isValidPlan = calenderQueryService.existsByPlanId(planId); + if (!isValidPlan) { + addConstraintViolation(context, PLAN_NOT_FOUND.getMessage()); + return false; + } + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + boolean isExistUserPlan = calenderQueryService.existsByUserIdAndPlanId(Long.valueOf(authentication.getName()), planId); + if (!isExistUserPlan) { + addConstraintViolation(context, USER_NOT_PART_OF_PLAN.getMessage()); + return false; + } + + return true; + } + + private void addConstraintViolation(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + } +} diff --git a/src/main/resources/application-database.yml b/src/main/resources/application-database.yml new file mode 100644 index 00000000..1dfc615e --- /dev/null +++ b/src/main/resources/application-database.yml @@ -0,0 +1,30 @@ +# Local Profile +spring: + config: + activate: + on-profile: local + datasource: + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + url: ${DB_URL} +--- +# Dev Profile +spring: + config: + activate: + on-profile: dev + datasource: + username: ${DEV_DB_USERNAME} + password: ${DEV_DB_PASSWORD} + url: ${DEV_DB_URL} +--- +# Release Profile +spring: + config: + activate: + on-profile: release + datasource: + username: ${RELEASE_DB_USERNAME} + password: ${RELEASE_DB_PASSWORD} + url: ${RELEASE_DB_URL} +--- diff --git a/src/main/resources/application-discord.yml b/src/main/resources/application-discord.yml new file mode 100644 index 00000000..b60f4e3e --- /dev/null +++ b/src/main/resources/application-discord.yml @@ -0,0 +1,2 @@ +spring: + discord-url: ${DISCORD_URL} \ No newline at end of file diff --git a/src/main/resources/application-firebase.yml b/src/main/resources/application-firebase.yml new file mode 100644 index 00000000..9f053fd4 --- /dev/null +++ b/src/main/resources/application-firebase.yml @@ -0,0 +1,3 @@ +spring: + firebase: + config: ${FIRE_BASE_CONFIG} \ No newline at end of file diff --git a/src/main/resources/application-image.yml b/src/main/resources/application-image.yml new file mode 100644 index 00000000..83386d41 --- /dev/null +++ b/src/main/resources/application-image.yml @@ -0,0 +1,3 @@ +app: + s3: + logo-url: ${LOGO_URL} diff --git a/src/main/resources/application-jwt.yml b/src/main/resources/application-jwt.yml new file mode 100644 index 00000000..511c9208 --- /dev/null +++ b/src/main/resources/application-jwt.yml @@ -0,0 +1,5 @@ +spring: + jwt: + secret: ${JWT_SECRET_KEY} + access-token-validity: ${JWT_ACCESS_TOKEN_TIME} + refresh-token-validity: ${JWT_REFRESH_TOKEN_TIME} \ No newline at end of file diff --git a/src/main/resources/application-mail.yml b/src/main/resources/application-mail.yml new file mode 100644 index 00000000..3a8e7eaa --- /dev/null +++ b/src/main/resources/application-mail.yml @@ -0,0 +1,17 @@ +spring: + mail: + host: ${EMAIL_HOST} + port: ${EMAIL_PORT} + username: ${EMAIL_USERNAME} + password: ${EMAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + connectiontimeout: 5000 + timeout: 5000 + writetimeout: 5000 + auth-code-expiration-millis: 1800000 \ No newline at end of file diff --git a/src/main/resources/application-oauth.yml b/src/main/resources/application-oauth.yml new file mode 100644 index 00000000..a3198e58 --- /dev/null +++ b/src/main/resources/application-oauth.yml @@ -0,0 +1,10 @@ +oauth: + kakao: + base-url: ${KAKAO_BASE_URL} + app-key: ${KAKAO_APP_KEY} + google: + base-url: ${GOOGLE_BASE_URL} + app-key: ${GOOGLE_CLIENT_ID} + apple: + base-url: ${APPLE_BASE_URL} + app-key: ${APPLE_TEAM_ID} \ No newline at end of file diff --git a/src/main/resources/application-redis.yml b/src/main/resources/application-redis.yml new file mode 100644 index 00000000..28d14e9d --- /dev/null +++ b/src/main/resources/application-redis.yml @@ -0,0 +1,30 @@ +# Local Profile +spring: + config: + activate: + on-profile: local + data: + redis: + host: localhost + port: 6379 +--- +# Dev Profile +spring: + config: + activate: + on-profile: dev + data: + redis: + host: ${REDIS_URL} + port: 6379 +--- +# Release Profile +spring: + config: + activate: + on-profile: release + data: + redis: + host: ${RELEASE_REDIS_URL} + port: 6379 +--- diff --git a/src/main/resources/application-s3.yml b/src/main/resources/application-s3.yml new file mode 100644 index 00000000..221bb72c --- /dev/null +++ b/src/main/resources/application-s3.yml @@ -0,0 +1,11 @@ +cloud: + aws: + s3: + bucket: ${S3_BUCKET} + credentials: + accessKey: ${S3_ACCESS_KEY} + secretKey: ${S3_SECRET_KEY} + region: + static: ap-northeast-2 + stack: + auto: false \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0566cb29..ffe8afe9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,14 +1,34 @@ +server: + port: 8080 + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true + spring: - datasource: - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - url: jdbc:mysql://localhost:3306/moim - driver-class-name: com.mysql.cj.jdbc.Driver + application: + name: moim jpa: + show-sql: true + generate-ddl: true properties: hibernate: - show_sql: true - format_sql: true - use_sql_comments: true hbm2ddl: - auto: update \ No newline at end of file + auto: none + format_sql: false + use_sql_comments: true + default_batch_fetch_size: 1000 + dialect: org.hibernate.dialect.MySQLDialect + database: mysql + profiles: + include: + - database + - jwt + - redis + - oauth + - mail + - discord + - s3 + - firebase + - image