diff --git a/.ebextensions/01_install_redis_cli.config b/.ebextensions/01_install_redis_cli.config new file mode 100644 index 0000000..d289fa6 --- /dev/null +++ b/.ebextensions/01_install_redis_cli.config @@ -0,0 +1,4 @@ +commands: + 01_install_redis_cli: + command: "sudo yum install -y redis" + ignoreErrors: "false" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 49e3f3d..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,63 +0,0 @@ -#name: aws-v5 -#on: -# push: -# branches: -# - feat/beanstalkdr -# -#jobs: -# build: -# runs-on: ubuntu-latest # 최초 ubuntu를 설치 -# steps: -# - name: Checkout -# uses: actions/checkout@v3 -# - name: Set up JDK 17 -# uses: actions/setup-java@v3 -# with: -# java-version: '17' -# distribution: 'temurin' -# - name: Grant execute permission for gradlew -# run: chmod +x ./gradlew -# -# - name: make env.properties -# if: | -# contains(github.ref, 'main') || -# contains(github.ref, 'dev') || -# contains(github.ref, 'feat') -# run: | -# touch env.properties -# echo "${{ secrets.ENV_PROPERTIES }}" > env.properties -# shell: bash -# -# - name: Build with Gradle -# run: ./gradlew clean build # 빌드 -# -# # UTC가 기준이기 때문에 한국시간으로 맞추려면 +9시간 해야 한다. -# - name: Get current time -# uses: 1466587594/get-current-time@v2 # 타임존 설정 -# id: current-time -# with: -# format: YYYY-MM-DDTHH-mm-ss -# utcOffset: "+09:00" # 시간 확인 -# -# - name: Show Current Time -# run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" # 시간 표시 -# - name: Generate deployment package -# run: | -# mkdir -p deploy -# cp build/libs/*.jar deploy/application.jar -# cp Procfile deploy/Procfile -# cp -r .ebextensions deploy/.ebextensions -# cp -r .platform deploy/.platform -# cd deploy && zip -r deploy.zip . -# -# - name: Deploy to EB -# uses: einaregilsson/beanstalk-deploy@v21 # 엘라스틱 빈스톡 환경 사용 -# with: -# aws_access_key: ${{ secrets.AWS_ACCESS_KEY }} # 중괄호 2개는 깃헙 환경변수 접근 -# aws_secret_key: ${{ secrets.AWS_SECRET_KEY }} -# application_name: moit # 엘리스틱 빈스톡 애플리케이션 이름! -# environment_name: Moit-env-3 # 엘리스틱 빈스톡 환경 이름! -# version_label: github-action-${{steps.current-time.outputs.formattedTime}} -# region: ap-northeast-2 # 서울 서버로 압축파일을 전달해줌 -# deployment_package: deploy/deploy.zip -# wait_for_environment_recovery: 60 # 배포 후 환경 복구 대기 시간 \ No newline at end of file diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index 12aff9b..0000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,119 +0,0 @@ -#name: CI/CD -# -#on: -# pull_request: -# branches: [ "main", "dev" ] -# types: -# - closed -# -#jobs: -# CI-CD: -# runs-on: ubuntu-latest -# -# steps: -# - uses: actions/checkout@v3 -# - name: Set up JDK 17 -# uses: actions/setup-java@v3 -# with: -# java-version: '17' -# distribution: 'temurin' -# -# # gradle caching - 빌드 시간 향상 -# - name: Gradle Caching -# uses: actions/cache@v3 -# with: -# path: | -# ~/.gradle/caches -# ~/.gradle/wrapper -# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} -# restore-keys: | -# ${{ runner.os }}-gradle- -# -# - name: Grant execute permission for gradlew -# run: chmod +x ./gradlew -# -# - name: make env.properties -# if: | -# contains(github.ref, 'main') || -# contains(github.ref, 'dev') -# run: | -# touch env.properties -# echo "${{ secrets.ENV_PROPERTIES }}" > env.properties -# shell: bash -# -# -# -# - name: Build with Gradle -# run: ./gradlew build -x test -# -# # 현재시간가져오기 -# - name: Get current time -# uses: josStorer/get-current-time@v2.0.2 -# id: current-time -# with: -# format: YYYY-MM-DDTHH-mm-ss -# utcOffset: "+09:00" -# -# # docker build & push -# # Dockerfile을 통해 이미지화 -# # push to docker hub -# - name: Docker build & push to dev -# # if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true -# if: | -# contains(github.ref, 'main') || -# contains(github.ref, 'dev') -# run: | -# docker login -u ${{ secrets.DOCKER_EMAIL }} -p ${{ secrets.DOCKER_PASSWORD }} -# docker build -t ${{ secrets.DOCKER_USERNAME }}/moit . -# docker push ${{ secrets.DOCKER_USERNAME }}/moit -# -# ## pull to develop -# - name: Pull to dev -# uses: appleboy/ssh-action@master -# id: pull-dev -## if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true -# if: | -# contains(github.ref, 'main') || -# contains(github.ref, 'dev') -# with: -# host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS -# username: ubuntu # ubuntu -# port: 22 -# key: ${{ secrets.PRIVATE_KEY }} -# envs: GITHUB_SHA -# script: | -# sudo docker ps -# sudo docker pull ${{ secrets.DOCKER_USERNAME }}/moit -# -# -# docker-pull-and-run: -# runs-on: [self-hosted, dev] -# if: github.event.pull_request.merged == true && ${{ needs.CI-CD.result == 'success' }} -# needs: [ CI-CD ] -# steps: -# - name: ✨ 배포 스크립트 실행 -# run: | -# sh /deploy.sh -# sudo docker image prune -f -# -## ## pull to develop -## - name: Pull to dev -## uses: appleboy/ssh-action@master -## id: pull-dev -## # if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true -## if: | -## contains(github.ref, 'main') || -## contains(github.ref, 'dev') -## with: -## host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS -## username: ubuntu # ubuntu -## port: 22 -## key: ${{ secrets.PRIVATE_KEY }} -## envs: GITHUB_SHA -## script: | -## sudo docker ps -## docker stop $(docker ps -a -q) -## docker rm $(docker ps -a -q) -## sudo docker pull ${{ secrets.DOCKER_USERNAME }}/moit -## sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/moit -## sudo docker image prune -f diff --git a/.github/workflows/divide.yml b/.github/workflows/divide.yml deleted file mode 100644 index 4605acd..0000000 --- a/.github/workflows/divide.yml +++ /dev/null @@ -1,95 +0,0 @@ -#name: beans-docker -# -#on: -# push: -# branches: [ "MOITBE-71-feat/ealsticache" ] -# -#jobs: -# CI-CD: -# runs-on: ubuntu-latest -# -# steps: -# - uses: actions/checkout@v3 -# - name: Set up JDK 17 -# uses: actions/setup-java@v3 -# with: -# java-version: '17' -# distribution: 'temurin' -# -# # gradle caching - 빌드 시간 향상 -# - name: Gradle Caching -# uses: actions/cache@v3 -# with: -# path: | -# ~/.gradle/caches -# ~/.gradle/wrapper -# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} -# restore-keys: | -# ${{ runner.os }}-gradle- -# -# - name: Grant execute permission for gradlew -# run: chmod +x ./gradlew -# -# - name: make env.properties -# run: | -# touch env.properties -# echo "${{ secrets.ENV_PROPERTIES }}" > env.properties -# shell: bash -# -# - name: make application-dev -# run: | -# mkdir -p src/main/resources -# cd ./src/main/resources -# touch ./application-dev.properties -# echo "${{ secrets.APPLICATION_DEV }}" > ./application-dev.properties -# shell: bash -# -# -# - name: Build with Gradle -# run: ./gradlew build -x test -# -# # 현재시간가져오기 -# - name: Get current time -# uses: josStorer/get-current-time@v2.0.2 -# id: current-time -# with: -# format: YYYY-MM-DDTHH-mm-ss -# utcOffset: "+09:00" -# -# # docker build & push -# # Dockerfile을 통해 이미지화 -# # push to docker hub -# - name: Docker build & push to dev -# run: | -# docker login -u ${{ secrets.DOCKER_EMAIL }} -p ${{ secrets.DOCKER_PASSWORD }} -# docker build -t ${{ secrets.DOCKER_USERNAME }}/moit . -# docker push ${{ secrets.DOCKER_USERNAME }}/moit -# -# beanstalk: -# name: Deploy to EB -# runs-on: ubuntu-latest -# needs: [ CI-CD ] -# steps: -# - name: Checkout -# uses: actions/checkout@v3 -# -# - name: Get current time -# uses: gerred/actions/current-time@master -# id: current-time -# -# - name: Use current time -# env: -# TIME: "${{ steps.current-time.outputs.time }}" -# run: echo $TIME -# -# - name: Deploy to EB -# uses: einaregilsson/beanstalk-deploy@v21 -# with: -# aws_access_key: ${{ secrets.AWS_ACCESS_KEY }} -# aws_secret_key: ${{ secrets.AWS_SECRET_KEY }} -# application_name: moit-docker -# environment_name: Moit-docker-env -# version_label: docker-${{ steps.current-time.outputs.time }} -# region: ap-northeast-2 -# deployment_package: elasticbeanstalk/docker-compose.yml -# wait_for_environment_recovery: 180 \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 38ff2b4..7c8245c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,14 +32,14 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: make env.properties - if: | - contains(github.ref, 'main') || - contains(github.ref, 'dev') - run: | - touch env.properties - echo "${{ secrets.ENV_PROPERTIES }}" > env.properties - shell: bash +# - name: make env.properties +# if: | +# contains(github.ref, 'main') || +# contains(github.ref, 'dev') +# run: | +# touch env.properties +# echo "${{ secrets.ENV_PROPERTIES }}" > env.properties +# shell: bash - name: make application-dev if: | @@ -55,14 +55,6 @@ jobs: - name: Build with Gradle run: ./gradlew build -x test - # 현재시간가져오기 - - name: Get current time - uses: josStorer/get-current-time@v2.0.2 - id: current-time - with: - format: YYYY-MM-DDTHH-mm-ss - utcOffset: "+09:00" - # docker build & push # Dockerfile을 통해 이미지화 # push to docker hub diff --git a/.github/workflows/feat.yml b/.github/workflows/feat.yml index 1579b8b..db5973e 100644 --- a/.github/workflows/feat.yml +++ b/.github/workflows/feat.yml @@ -33,11 +33,11 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x ./gradlew - - name: make env.properties - run: | - touch env.properties - echo "${{ secrets.ENV_PROPERTIES }}" > env.properties - shell: bash +# - name: make env.properties +# run: | +# touch env.properties +# echo "${{ secrets.ENV_PROPERTIES }}" > env.properties +# shell: bash - name: Build with Gradle run: ./gradlew build -x test diff --git a/.gitignore b/.gitignore index c915d95..957e0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,12 @@ gradle-app.setting .idea/ env.properties prometheus.yml +application-dev.properties +application-local.properties +application-sql.properties + +### Logging files ### +/log # End of https://www.toptal.com/developers/gitignore/api/windows,macos,intellij,gradle /.idea/.name diff --git a/Dockerfile b/Dockerfile index 07e2600..6d8c6f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM openjdk:17-alpine -COPY env.properties /env.properties +#COPY env.properties /env.properties COPY src/main/resources/application-dev.properties /src/main/resources/application-dev.properties ARG JAR_FILE=build/libs/*.jar diff --git a/build.gradle b/build.gradle index ea9ac4c..bed6494 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' // actuator implementation 'io.micrometer:micrometer-registry-prometheus' + // postgresql, postgis 관련 util + implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.4' + implementation 'org.hibernate:hibernate-spatial:6.4.4.Final' + implementation group: 'org.hibernate.orm', name: 'hibernate-spatial', version: '6.4.4.Final' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' @@ -75,5 +80,4 @@ tasks.named('test') { jar { enabled = false -} - +} \ No newline at end of file diff --git a/elasticbeanstalk/docker-compose.yml b/elasticbeanstalk/docker-compose.yml index 40e09f9..a71dcfe 100644 --- a/elasticbeanstalk/docker-compose.yml +++ b/elasticbeanstalk/docker-compose.yml @@ -4,4 +4,26 @@ services: image: "moit03/moit:latest" ports: - "80:8080" - restart: "always" \ No newline at end of file + restart: "always" + volumes: + - '/var/log:/log' + + node_exporter: + image: quay.io/prometheus/node-exporter:latest + container_name: node_exporter + command: + - '--path.rootfs=/host' + network_mode: host + pid: host + restart: unless-stopped + volumes: + - '/:/host:ro,rslave' + +# promtail: +# image: grafana/promtail:latest +# volumes: +# - /etc/promtail/promtail.yml:/etc/promtail/promtail.yml # 호스트 시스템의 설정 파일을 컨테이너 내부에 마운트 +# command: -config.file=/etc/promtail/promtail.yml +# ports: +# - "9080:9080" # Promtail HTTP endpoint +# restart: unless-stopped \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/MoitApplication.java b/src/main/java/com/sparta/moit/MoitApplication.java index 8b45e74..c80bf70 100644 --- a/src/main/java/com/sparta/moit/MoitApplication.java +++ b/src/main/java/com/sparta/moit/MoitApplication.java @@ -7,12 +7,16 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +import java.util.TimeZone; + @OpenAPIDefinition(servers = {@Server(url = "https://hhboard.xyz", description = "Default Server URL")}) @EnableJpaAuditing @SpringBootApplication @EnableScheduling public class MoitApplication { + public static void main(String[] args) { SpringApplication.run(MoitApplication.class, args); +// TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } } diff --git a/src/main/java/com/sparta/moit/domain/bookmark/controller/BookMarkController.java b/src/main/java/com/sparta/moit/domain/bookmark/controller/BookMarkController.java new file mode 100644 index 0000000..94bc797 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/controller/BookMarkController.java @@ -0,0 +1,91 @@ +package com.sparta.moit.domain.bookmark.controller; + +import com.sparta.moit.domain.bookmark.controller.docs.BookMarkControllerDocs; +import com.sparta.moit.domain.bookmark.dto.BookMarkRequestDto; +import com.sparta.moit.domain.bookmark.dto.BookMarkResponseDto; +import com.sparta.moit.domain.bookmark.dto.GetBookMarkResponseDto; +import com.sparta.moit.domain.bookmark.entity.BookMark; +import com.sparta.moit.domain.bookmark.repository.BookMarkRepository; +import com.sparta.moit.domain.bookmark.service.BookMarkService; +import com.sparta.moit.domain.member.entity.Member; +import com.sparta.moit.domain.member.repository.MemberRepository; +import com.sparta.moit.global.common.dto.ResponseDto; +import com.sparta.moit.global.security.UserDetailsImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/bookmark") +@RequiredArgsConstructor +public class BookMarkController implements BookMarkControllerDocs { + private final BookMarkService bookMarkService; + private final MemberRepository memberRepository; + private final BookMarkRepository bookMarkRepository; + + @PostMapping("/add") + public ResponseEntity> addMeetingBookmark(@RequestBody BookMarkRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) { + Member member = userDetails.getUser(); + BookMarkResponseDto bookmarkResponseDto = new BookMarkResponseDto(requestDto.getMeetingId(), member.getId()); + + bookMarkService.addMeetingBookmark(bookmarkResponseDto, member); + return ResponseEntity.ok().body(ResponseDto.success("북마크 완료", bookmarkResponseDto)); + } + + @DeleteMapping("/remove") + public ResponseEntity> removeMeetingBookmark(@RequestBody BookMarkRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) { + Member member = userDetails.getUser(); + BookMarkResponseDto bookmarkResponseDto = new BookMarkResponseDto(requestDto.getMeetingId(), member.getId()); + + bookMarkService.removeMeetingBookmark(bookmarkResponseDto, member); + return ResponseEntity.ok().body(ResponseDto.success("북마크 해제", bookmarkResponseDto)); + } + + /** + * 해당 모임이 북마크 되어있는지 확인 + * + * @param meetingId 확인할 모임 ID + * @param userDetails 현재 인증된 회원 정보 + * @return 즐겨찾기에 있으면 true, 없으면 false + */ + @GetMapping("/check") + public ResponseEntity isBookmarked(@RequestParam Long meetingId, @AuthenticationPrincipal UserDetailsImpl userDetails) { + if (userDetails == null || userDetails.getUser() == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(false); + } + Member member = userDetails.getUser(); + BookMarkResponseDto bookmarkResponseDto = new BookMarkResponseDto(meetingId, member.getId()); + + boolean isBookmarked = bookMarkService.isBookmarked(bookmarkResponseDto, member); + return ResponseEntity.ok(isBookmarked); + } + + /** + * + * @param userDetails 현재 인증된 회원 정보 + * @return 즐겨찾기에 있으면 true, 없으면 false + */ + @GetMapping("/checkByMemberId") + public ResponseEntity> isBookmarkedByMemberId(@AuthenticationPrincipal UserDetailsImpl userDetails) { + Long memberId = userDetails.getUser().getId(); + + try { + List bookmarks = bookMarkRepository.findByMemberId(memberId); + + List bookmarkedMeetingIds = bookmarks.stream() + .map(bookmark -> bookmark.getMeeting().getId()) + .toList(); + + GetBookMarkResponseDto responseDto = new GetBookMarkResponseDto(bookmarkedMeetingIds); + + return ResponseEntity.ok().body(ResponseDto.success("유저가 북마크한 모임 조회 완료", responseDto)); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.fail("북마크 조회 실패", null)); + } + } +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/controller/docs/BookMarkControllerDocs.java b/src/main/java/com/sparta/moit/domain/bookmark/controller/docs/BookMarkControllerDocs.java new file mode 100644 index 0000000..220842d --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/controller/docs/BookMarkControllerDocs.java @@ -0,0 +1,23 @@ +package com.sparta.moit.domain.bookmark.controller.docs; + +import com.sparta.moit.domain.bookmark.dto.BookMarkRequestDto; +import com.sparta.moit.domain.bookmark.dto.BookMarkResponseDto; +import com.sparta.moit.domain.bookmark.dto.GetBookMarkResponseDto; +import com.sparta.moit.global.common.dto.ResponseDto; +import com.sparta.moit.global.security.UserDetailsImpl; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +public interface BookMarkControllerDocs { + @Operation(summary = "북마크 추가", description = "북마크 추가 API") + ResponseEntity> addMeetingBookmark(@RequestBody BookMarkRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "북마크 해제", description = "북마크 해제 API") + ResponseEntity> removeMeetingBookmark(@RequestBody BookMarkRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "북마크 확인", description = "북마크 확인 API") + ResponseEntity isBookmarked(@RequestParam Long meetingId, @AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "유저 북마크 여부", description = "유저가 북마크한 모임 조회 API") + ResponseEntity> isBookmarkedByMemberId(@AuthenticationPrincipal UserDetailsImpl userDetails); +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/dto/BookMarkRequestDto.java b/src/main/java/com/sparta/moit/domain/bookmark/dto/BookMarkRequestDto.java new file mode 100644 index 0000000..9d73149 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/dto/BookMarkRequestDto.java @@ -0,0 +1,14 @@ +package com.sparta.moit.domain.bookmark.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +@Getter +@NoArgsConstructor +public class BookMarkRequestDto { + private Long meetingId; + public BookMarkRequestDto(Long meetingId) { + this.meetingId = meetingId; + } +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/dto/BookMarkResponseDto.java b/src/main/java/com/sparta/moit/domain/bookmark/dto/BookMarkResponseDto.java new file mode 100644 index 0000000..1cf9640 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/dto/BookMarkResponseDto.java @@ -0,0 +1,12 @@ +package com.sparta.moit.domain.bookmark.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class BookMarkResponseDto { + private final Long meetingId; + private final Long memberId; + +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/dto/GetBookMarkResponseDto.java b/src/main/java/com/sparta/moit/domain/bookmark/dto/GetBookMarkResponseDto.java new file mode 100644 index 0000000..9adaa6b --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/dto/GetBookMarkResponseDto.java @@ -0,0 +1,12 @@ +package com.sparta.moit.domain.bookmark.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class GetBookMarkResponseDto { + private final List bookmarkedMeetingIds; +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/entity/BookMark.java b/src/main/java/com/sparta/moit/domain/bookmark/entity/BookMark.java new file mode 100644 index 0000000..b5ba249 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/entity/BookMark.java @@ -0,0 +1,32 @@ +package com.sparta.moit.domain.bookmark.entity; + +import com.sparta.moit.domain.meeting.entity.Meeting; +import com.sparta.moit.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BookMark { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meeting_id") + private Meeting meeting; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Builder + public BookMark(Meeting meeting, Member member) { + this.meeting = meeting; + this.member = member; + } +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/repository/BookMarkRepository.java b/src/main/java/com/sparta/moit/domain/bookmark/repository/BookMarkRepository.java new file mode 100644 index 0000000..b69baa0 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/repository/BookMarkRepository.java @@ -0,0 +1,19 @@ +package com.sparta.moit.domain.bookmark.repository; + +import com.sparta.moit.domain.bookmark.entity.BookMark; +import com.sparta.moit.domain.meeting.entity.Meeting; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface BookMarkRepository extends JpaRepository { + Optional findByMeetingIdAndMemberId(Long meetingId, Long memberId); + @Query("SELECT b.meeting FROM BookMark b WHERE b.member.id = :memberId and b.meeting.status != 'DELETE' " + + "ORDER BY CASE WHEN b.meeting.status = 'OPEN' OR b.meeting.status = 'FULL' THEN b.meeting.meetingDate END ASC, " + + "CASE WHEN b.meeting.status = 'COMPLETE' THEN b.meeting.meetingDate END DESC") + List findBookmarkedMeetingsByMemberId(Long memberId); + boolean existsByMemberIdAndMeetingId(Long memberId, Long meetingId); + List findByMemberId(Long memberId); +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/service/BookMarkService.java b/src/main/java/com/sparta/moit/domain/bookmark/service/BookMarkService.java new file mode 100644 index 0000000..0487c11 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/service/BookMarkService.java @@ -0,0 +1,11 @@ +package com.sparta.moit.domain.bookmark.service; + +import com.sparta.moit.domain.bookmark.dto.BookMarkResponseDto; +import com.sparta.moit.domain.member.entity.Member; + +public interface BookMarkService { + void addMeetingBookmark(BookMarkResponseDto bookmarkResponseDto, Member member); + void removeMeetingBookmark(BookMarkResponseDto bookmarkResponseDto, Member member); + boolean isBookmarked(BookMarkResponseDto bookmarkResponseDto, Member member); + +} diff --git a/src/main/java/com/sparta/moit/domain/bookmark/service/BookMarkServiceImpl.java b/src/main/java/com/sparta/moit/domain/bookmark/service/BookMarkServiceImpl.java new file mode 100644 index 0000000..ebd7385 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/bookmark/service/BookMarkServiceImpl.java @@ -0,0 +1,62 @@ +package com.sparta.moit.domain.bookmark.service; + +import com.sparta.moit.domain.bookmark.dto.BookMarkResponseDto; +import com.sparta.moit.domain.bookmark.entity.BookMark; +import com.sparta.moit.domain.bookmark.repository.BookMarkRepository; +import com.sparta.moit.domain.meeting.entity.Meeting; +import com.sparta.moit.domain.meeting.repository.MeetingRepository; +import com.sparta.moit.domain.member.entity.Member; +import com.sparta.moit.global.error.CustomException; +import com.sparta.moit.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BookMarkServiceImpl implements BookMarkService { + private final MeetingRepository meetingRepository; + private final BookMarkRepository bookMarkRepository; + + @Transactional + public void addMeetingBookmark(BookMarkResponseDto bookmarkResponseDto, Member member) { + if (member == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + if (isBookmarked(bookmarkResponseDto, member)) { + throw new CustomException(ErrorCode.ALREADY_BOOKMARKED); + } + Meeting meeting = meetingRepository.findById(bookmarkResponseDto.getMeetingId()) + .orElseThrow(() -> new CustomException(ErrorCode.MEETING_NOT_FOUND)); + + BookMark bookmark = BookMark.builder() + .meeting(meeting) + .member(member) + .build(); + + bookMarkRepository.save(bookmark); + } + + @Override + @Transactional + public void removeMeetingBookmark(BookMarkResponseDto bookmarkResponseDto, Member member) { + if (member == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + if (!isBookmarked(bookmarkResponseDto, member)) { + throw new CustomException(ErrorCode.NOT_BOOKMARKED); + } + BookMark bookmark = bookMarkRepository.findByMeetingIdAndMemberId(bookmarkResponseDto.getMeetingId(), member.getId()) + .orElseThrow(() -> new CustomException(ErrorCode.BOOKMARK_NOT_FOUND)); + + bookMarkRepository.delete(bookmark); + } + + @Override + public boolean isBookmarked(BookMarkResponseDto bookmarkResponseDto, Member member) { + if (member == null) { + return false; + } + return bookMarkRepository.findByMeetingIdAndMemberId(bookmarkResponseDto.getMeetingId(), member.getId()).isPresent(); + } +} diff --git a/src/main/java/com/sparta/moit/domain/chat/controller/ChatController.java b/src/main/java/com/sparta/moit/domain/chat/controller/ChatController.java index 899c855..8fe1d75 100644 --- a/src/main/java/com/sparta/moit/domain/chat/controller/ChatController.java +++ b/src/main/java/com/sparta/moit/domain/chat/controller/ChatController.java @@ -8,6 +8,7 @@ import com.sparta.moit.global.security.UserDetailsImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.time.LocalDateTime; @Slf4j(topic = "채팅 Controller") @RestController @@ -26,11 +28,13 @@ public class ChatController implements ChatControllerDocs { private final SimpMessagingTemplate messagingTemplate; /* 채팅방 입장 전 채팅 목록 불러오기 */ - @GetMapping("/api/meetings/{meetingId}/chats") + @GetMapping("/api/meetings/{meetingId}/chats/{userEnterTime}") public ResponseEntity> getChatList(@PathVariable Long meetingId , @RequestParam(defaultValue = "1") int page + , @PathVariable("userEnterTime") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime userEnterTime , @AuthenticationPrincipal UserDetailsImpl userDetails) { - ChatResponseDto responseDto = chatService.getChatList(meetingId, page, userDetails.getUser()); + ChatResponseDto responseDto = chatService.getChatList(meetingId, page, userEnterTime, userDetails.getUser()); return ResponseEntity.ok().body(ResponseDto.success("채팅 불러오기 완료", responseDto)); } diff --git a/src/main/java/com/sparta/moit/domain/chat/controller/docs/ChatControllerDocs.java b/src/main/java/com/sparta/moit/domain/chat/controller/docs/ChatControllerDocs.java index 58d2f18..e77f5a6 100644 --- a/src/main/java/com/sparta/moit/domain/chat/controller/docs/ChatControllerDocs.java +++ b/src/main/java/com/sparta/moit/domain/chat/controller/docs/ChatControllerDocs.java @@ -3,17 +3,22 @@ import com.sparta.moit.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; +import java.time.LocalDateTime; + @Tag(name = "채팅", description = "채팅 API") public interface ChatControllerDocs { @Operation(summary = "채팅 목록 불러오기 기능", description = "채팅 불러오기 API") ResponseEntity getChatList(@PathVariable Long meetingId , @RequestParam(defaultValue = "1") int page + , @PathVariable("userEnterTime") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime userEnterTime , @AuthenticationPrincipal UserDetailsImpl userDetails); } diff --git a/src/main/java/com/sparta/moit/domain/chat/dto/ChatResponseDto.java b/src/main/java/com/sparta/moit/domain/chat/dto/ChatResponseDto.java index 475405b..5fd1077 100644 --- a/src/main/java/com/sparta/moit/domain/chat/dto/ChatResponseDto.java +++ b/src/main/java/com/sparta/moit/domain/chat/dto/ChatResponseDto.java @@ -1,6 +1,7 @@ package com.sparta.moit.domain.chat.dto; import com.sparta.moit.domain.chat.entity.Chat; +import com.sparta.moit.domain.meeting.entity.MeetingStatusEnum; import lombok.Builder; import lombok.Getter; import org.springframework.data.domain.Slice; @@ -14,15 +15,18 @@ public class ChatResponseDto { private final Long meetingId; + private final MeetingStatusEnum meetingStatusEnum; + private final Slice chats; @Builder - public ChatResponseDto(Long meetingId, Slice chats) { + public ChatResponseDto(Long meetingId, MeetingStatusEnum meetingStatusEnum, Slice chats) { this.meetingId = meetingId; + this.meetingStatusEnum = meetingStatusEnum; this.chats = chats; } - public static ChatResponseDto fromEntity(Slice chatEntitySlice, Long meetingId) { + public static ChatResponseDto fromEntity(Slice chatEntitySlice, Long meetingId, MeetingStatusEnum meetingStatusEnum) { List chatDtos = chatEntitySlice.getContent().stream() .map(SingleChatResponseDto::fromEntity) .collect(Collectors.toList()); @@ -31,6 +35,7 @@ public static ChatResponseDto fromEntity(Slice chatEntitySlice, Long meeti return ChatResponseDto.builder() .meetingId(meetingId) + .meetingStatusEnum(meetingStatusEnum) .chats(chatDtoSlice).build(); } } diff --git a/src/main/java/com/sparta/moit/domain/chat/dto/SendChatRequestDto.java b/src/main/java/com/sparta/moit/domain/chat/dto/SendChatRequestDto.java index 5b6930e..8d12f64 100644 --- a/src/main/java/com/sparta/moit/domain/chat/dto/SendChatRequestDto.java +++ b/src/main/java/com/sparta/moit/domain/chat/dto/SendChatRequestDto.java @@ -1,8 +1,10 @@ package com.sparta.moit.domain.chat.dto; +import jakarta.validation.constraints.Size; import lombok.Getter; @Getter public class SendChatRequestDto { + @Size(max = 10000, message = "채팅 내용은 최대 10,000자까지 입력 가능합니다.") private String content; } diff --git a/src/main/java/com/sparta/moit/domain/chat/dto/SendChatResponseDto.java b/src/main/java/com/sparta/moit/domain/chat/dto/SendChatResponseDto.java index 95f78ad..d18ec10 100644 --- a/src/main/java/com/sparta/moit/domain/chat/dto/SendChatResponseDto.java +++ b/src/main/java/com/sparta/moit/domain/chat/dto/SendChatResponseDto.java @@ -26,7 +26,7 @@ public static SendChatResponseDto fromEntity(Chat chat) { .chatId(chat.getId()) .sender(sender) .content(chat.getContent()) - .createdAt(chat.getCreatedAt()) + .createdAt(chat.getCreatedAt().plusHours(9)) .build(); } } diff --git a/src/main/java/com/sparta/moit/domain/chat/entity/Chat.java b/src/main/java/com/sparta/moit/domain/chat/entity/Chat.java index c277802..57faf3f 100644 --- a/src/main/java/com/sparta/moit/domain/chat/entity/Chat.java +++ b/src/main/java/com/sparta/moit/domain/chat/entity/Chat.java @@ -17,12 +17,13 @@ public class Chat extends Timestamped { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(length=10000) private String content; - @ManyToOne(fetch = FetchType.EAGER) + @ManyToOne private Member member; - @ManyToOne(fetch = FetchType.EAGER) + @ManyToOne private Meeting meeting; @Builder diff --git a/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepository.java b/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepository.java index e0641af..5f9b096 100644 --- a/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepository.java +++ b/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepository.java @@ -6,9 +6,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; +public interface ChatRepository extends JpaRepository, ChatRepositoryCustom { -public interface ChatRepository extends JpaRepository { - Slice findAllByMeetingOrderByIdDesc(Meeting meeting, Pageable pageable); } diff --git a/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepositoryCustom.java b/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepositoryCustom.java new file mode 100644 index 0000000..a839a74 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepositoryCustom.java @@ -0,0 +1,12 @@ +package com.sparta.moit.domain.chat.repository; + +import com.sparta.moit.domain.chat.entity.Chat; +import com.sparta.moit.domain.meeting.entity.Meeting; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.time.LocalDateTime; + +public interface ChatRepositoryCustom { + Slice getPreviousChats(Meeting meeting, LocalDateTime userEnterTime, Pageable pageable); +} diff --git a/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepositoryImpl.java b/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepositoryImpl.java new file mode 100644 index 0000000..6c1e61b --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/chat/repository/ChatRepositoryImpl.java @@ -0,0 +1,45 @@ +package com.sparta.moit.domain.chat.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.sparta.moit.domain.chat.entity.Chat; +import com.sparta.moit.domain.meeting.entity.Meeting; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.sparta.moit.domain.chat.entity.QChat.chat; + +@RequiredArgsConstructor +public class ChatRepositoryImpl implements ChatRepositoryCustom{ + private final JPAQueryFactory queryFactory; + @Override + public Slice getPreviousChats(Meeting meeting, LocalDateTime userEnterTime, Pageable pageable) { + + BooleanExpression createdBefore = chat.createdAt.before(userEnterTime); + BooleanExpression isCorrectMeeting = chat.meeting.eq(meeting); + + List chatList = queryFactory + .selectFrom(chat) + .where( + createdBefore, + isCorrectMeeting + ) + .orderBy(chat.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + boolean hasNext = false; + if (chatList.size() > pageable.getPageSize()) { + chatList.remove(pageable.getPageSize()); + hasNext = true; + } + + return new SliceImpl<>(chatList, pageable, hasNext); + } +} diff --git a/src/main/java/com/sparta/moit/domain/chat/service/ChatService.java b/src/main/java/com/sparta/moit/domain/chat/service/ChatService.java index 3f1fbc5..0ed4ff5 100644 --- a/src/main/java/com/sparta/moit/domain/chat/service/ChatService.java +++ b/src/main/java/com/sparta/moit/domain/chat/service/ChatService.java @@ -5,8 +5,10 @@ import com.sparta.moit.domain.chat.dto.SendChatResponseDto; import com.sparta.moit.domain.member.entity.Member; +import java.time.LocalDateTime; + public interface ChatService { - ChatResponseDto getChatList(Long meetingId, int page, Member member); + ChatResponseDto getChatList(Long meetingId, int page, LocalDateTime userEnterTime, Member member); SendChatResponseDto sendChat(Long meetingId, String email, SendChatRequestDto chatRequest); } diff --git a/src/main/java/com/sparta/moit/domain/chat/service/ChatServiceImpl.java b/src/main/java/com/sparta/moit/domain/chat/service/ChatServiceImpl.java index 3db0c87..ac06777 100644 --- a/src/main/java/com/sparta/moit/domain/chat/service/ChatServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/chat/service/ChatServiceImpl.java @@ -6,6 +6,7 @@ import com.sparta.moit.domain.chat.entity.Chat; import com.sparta.moit.domain.chat.repository.ChatRepository; import com.sparta.moit.domain.meeting.entity.Meeting; +import com.sparta.moit.domain.meeting.entity.MeetingStatusEnum; import com.sparta.moit.domain.meeting.repository.MeetingMemberRepository; import com.sparta.moit.domain.meeting.repository.MeetingRepository; import com.sparta.moit.domain.member.entity.Member; @@ -21,6 +22,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDateTime; + @Slf4j(topic = "채팅 로그") @RestController @RequestMapping("/api/chats") @@ -33,7 +36,7 @@ public class ChatServiceImpl implements ChatService { private final MemberRepository memberRepository; @Override - public ChatResponseDto getChatList(Long meetingId, int page, Member member) { + public ChatResponseDto getChatList(Long meetingId, int page, LocalDateTime userEnterTime, Member member) { /* * 해당 모임이 존재하는지 확인한다. * 해당 모임에 가입한 유져가 맞는 지 확인한다. @@ -42,6 +45,10 @@ public ChatResponseDto getChatList(Long meetingId, int page, Member member) { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new CustomException(ErrorCode.MEETING_NOT_FOUND)); + if (meeting.getStatus().equals(MeetingStatusEnum.DELETE)) { + throw new CustomException(ErrorCode.MEETING_NOT_FOUND); + } + if (!isMeetingMember(member, meeting)) { throw new CustomException(ErrorCode.NOT_MEETING_MEMBER); } @@ -49,9 +56,9 @@ public ChatResponseDto getChatList(Long meetingId, int page, Member member) { int CHAT_PAGE_SIZE = 20; Pageable pageable = PageRequest.of(Math.max(page - 1, 0), CHAT_PAGE_SIZE, Sort.by(Sort.Direction.DESC, "id")); - Slice chatList = chatRepository.findAllByMeetingOrderByIdDesc(meeting, pageable); + Slice chatList = chatRepository.getPreviousChats(meeting, userEnterTime, pageable); - return ChatResponseDto.fromEntity(chatList, meetingId); + return ChatResponseDto.fromEntity(chatList, meetingId, meeting.getStatus()); } @Override @@ -68,13 +75,19 @@ public SendChatResponseDto sendChat(Long meetingId, String email, SendChatReques Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new CustomException(ErrorCode.MEETING_NOT_FOUND)); + if (meeting.getStatus().equals(MeetingStatusEnum.COMPLETE)){ + throw new CustomException(ErrorCode.MEETING_NOT_FOUND); + } + if (!isMeetingMember(testMember, meeting)) { throw new CustomException(ErrorCode.NOT_MEETING_MEMBER); } + Chat chat = Chat.builder() .content(sendChatRequestDto.getContent()) .member(testMember) .meeting(meeting) +// .createdAt(LocalDateTime.now()) .build(); chatRepository.save(chat); diff --git a/src/main/java/com/sparta/moit/domain/meeting/controller/MeetingController.java b/src/main/java/com/sparta/moit/domain/meeting/controller/MeetingController.java index 7b2bf3e..cdf5e2c 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/controller/MeetingController.java +++ b/src/main/java/com/sparta/moit/domain/meeting/controller/MeetingController.java @@ -1,10 +1,7 @@ package com.sparta.moit.domain.meeting.controller; import com.sparta.moit.domain.meeting.controller.docs.MeetingControllerDocs; -import com.sparta.moit.domain.meeting.dto.CreateMeetingRequestDto; -import com.sparta.moit.domain.meeting.dto.GetMeetingDetailResponseDto; -import com.sparta.moit.domain.meeting.dto.GetMeetingResponseDto; -import com.sparta.moit.domain.meeting.dto.UpdateMeetingRequestDto; +import com.sparta.moit.domain.meeting.dto.*; import com.sparta.moit.domain.meeting.service.MeetingService; import com.sparta.moit.domain.member.entity.Member; import com.sparta.moit.global.common.dto.ResponseDto; @@ -28,18 +25,31 @@ public class MeetingController implements MeetingControllerDocs { private final MeetingService meetingService; /*모임 등록*/ - @PostMapping + @PostMapping("/json") public ResponseEntity> createMeeting(@RequestBody CreateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) { Long meetingId = meetingService.createMeeting(requestDto, userDetails.getUser()); return ResponseEntity.ok().body(ResponseDto.success("모임 등록 완료", meetingId)); } + /*모임 등록*/ + @PostMapping + public ResponseEntity> createMeetingArray(@RequestBody CreateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) { + Long meetingId = meetingService.createMeetingArray(requestDto, userDetails.getUser()); + return ResponseEntity.ok().body(ResponseDto.success("모임 등록 완료", meetingId)); + } + /*모임 수정*/ - @PutMapping("/{meetingId}") + @PutMapping("/json/{meetingId}") public ResponseEntity> updateMeeting(@PathVariable Long meetingId, @RequestBody UpdateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) { Long updatedMeetingId = meetingService.updateMeeting(requestDto, userDetails.getUser(), meetingId); return ResponseEntity.ok().body(ResponseDto.success("모임 수정 완료", updatedMeetingId)); } + /*모임 array 수정*/ + @PutMapping("/{meetingId}") + public ResponseEntity> updateMeetingArray(@PathVariable Long meetingId, @RequestBody UpdateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) { + Long updatedMeetingId = meetingService.updateMeetingArray(requestDto, userDetails.getUser(), meetingId); + return ResponseEntity.ok().body(ResponseDto.success("모임 수정 완료", updatedMeetingId)); + } /*모임 삭제*/ @DeleteMapping("/{meetingId}") @@ -48,8 +58,55 @@ public ResponseEntity> deleteMeeting(@PathVariable Long meet return ResponseEntity.ok().body(ResponseDto.success("모임 삭제 완료", "삭제")); } - /*모임 조회*/ + @GetMapping("/json") + public ResponseEntity>> getMeetingListPostgreJson( + @RequestParam Double locationLat, + @RequestParam Double locationLng, + @RequestParam(required = false) List skillId, + @RequestParam(required = false) List careerId, + @RequestParam(defaultValue = "1") int page) + { + + String skillIdsStr = (skillId == null || skillId.isEmpty()) ? null : String.join(",", skillId); + String careerIdsStr = (careerId == null || careerId.isEmpty()) ? null : String.join(",", careerId); + + Slice responseDtoList = meetingService.getMeetingListPostgreJson(page + , locationLat + , locationLng + , skillIdsStr + , careerIdsStr + ); + + return ResponseEntity.ok().body(ResponseDto.success("조회 완료", responseDtoList)); + } + @GetMapping + public ResponseEntity>> getMeetingListPostgreArray( + @RequestParam Double locationLat, + @RequestParam Double locationLng, + @RequestParam(required = false) List skillId, + @RequestParam(required = false) List careerId, + @RequestParam(defaultValue = "1") int page) + { + + String skillIdsStr = (skillId == null || skillId.isEmpty()) ? null : String.join(",", skillId); + String careerIdsStr = (careerId == null || careerId.isEmpty()) ? null : String.join(",", careerId); + + Slice responseDtoList = meetingService.getMeetingListPostgreArray( + page + , locationLat + , locationLng + , skillIdsStr + , careerIdsStr + ); + + return ResponseEntity.ok().body(ResponseDto.success("조회 완료", responseDtoList)); + } + + + + /*모임 조회*/ + @GetMapping("/mysql") public ResponseEntity>> getMeetingList (@RequestParam Double locationLat, @RequestParam Double locationLng, @@ -85,11 +142,18 @@ public ResponseEntity> getMeetingDetail /* 모임 검색 */ @GetMapping("/search") - public ResponseEntity>> getMeetingListBySearch(@RequestParam String keyword, @RequestParam(defaultValue = "1") int page) { - Slice responseDtoList = meetingService.getMeetingListBySearch(keyword, page); + public ResponseEntity>> getMeetingListBySearch(@RequestParam String keyword, @RequestParam(defaultValue = "1") int page) { + Slice responseDtoList = meetingService.getMeetingListBySearch(keyword, page); return ResponseEntity.ok().body(ResponseDto.success("검색 완료", responseDtoList)); } + /* 인기 모임 top 5 */ + @GetMapping("/popular") + public ResponseEntity getPopularMeeting() { + List responseDtoList = meetingService.getPopularMeeting(); + return ResponseEntity.ok().body(ResponseDto.success("인기 모임 top 5", responseDtoList)); + } + /*모임 참가*/ @PostMapping("my-meetings/{meetingId}") public ResponseEntity> enterMeeting(@PathVariable Long meetingId, @AuthenticationPrincipal UserDetailsImpl userDetails) { diff --git a/src/main/java/com/sparta/moit/domain/meeting/controller/RegionController.java b/src/main/java/com/sparta/moit/domain/meeting/controller/RegionController.java index 2e73a95..cd5a33b 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/controller/RegionController.java +++ b/src/main/java/com/sparta/moit/domain/meeting/controller/RegionController.java @@ -2,6 +2,7 @@ import com.sparta.moit.domain.meeting.controller.docs.RegionControllerDocs; import com.sparta.moit.domain.meeting.dto.RegionFirstResponseDto; +import com.sparta.moit.domain.meeting.dto.RegionIntegratedResponseDto; import com.sparta.moit.domain.meeting.dto.RegionSecondResponseDto; import com.sparta.moit.domain.meeting.service.RegionService; import com.sparta.moit.global.common.dto.ResponseDto; @@ -20,6 +21,12 @@ public class RegionController implements RegionControllerDocs { private final RegionService regionService; + @GetMapping() + public ResponseEntity getRegion() { + List responseDto = regionService.getRegion(); + return ResponseEntity.ok().body(ResponseDto.success("시-도 정보 통합 조회 성공", responseDto)); + } + @GetMapping("/first") public ResponseEntity getRegionFirst(){ List responseDto = regionService.getRegionFirst(); diff --git a/src/main/java/com/sparta/moit/domain/meeting/controller/docs/MeetingControllerDocs.java b/src/main/java/com/sparta/moit/domain/meeting/controller/docs/MeetingControllerDocs.java index 26b776f..892e8e2 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/controller/docs/MeetingControllerDocs.java +++ b/src/main/java/com/sparta/moit/domain/meeting/controller/docs/MeetingControllerDocs.java @@ -4,6 +4,7 @@ import com.sparta.moit.domain.meeting.dto.CreateMeetingRequestDto; import com.sparta.moit.domain.meeting.dto.GetMeetingDetailResponseDto; import com.sparta.moit.domain.meeting.dto.UpdateMeetingRequestDto; +import com.sparta.moit.global.common.dto.ResponseDto; import com.sparta.moit.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -13,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; @@ -24,9 +26,16 @@ public interface MeetingControllerDocs { @Operation(summary = "모임 등록 기능", description = "모임 등록 API") ResponseEntity createMeeting(@RequestBody CreateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "모임 등록 기능", description = "모임 등록 API") + ResponseEntity createMeetingArray(@RequestBody CreateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails); + + @Operation(summary = "모임 수정 기능", description = "모임 수정 API") ResponseEntity updateMeeting(@PathVariable Long meetingId, @RequestBody UpdateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "모임 수정 array 기능", description = "모임 수정 array API") + @PutMapping("/array/{meetingId}") + public ResponseEntity> updateMeetingArray(@PathVariable Long meetingId, @RequestBody UpdateMeetingRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails); @Operation(summary = "모임 삭제 기능", description = "모임 삭제 API") ResponseEntity deleteMeeting(@PathVariable Long meetingId, @AuthenticationPrincipal UserDetailsImpl userDetails); @@ -53,6 +62,9 @@ public ResponseEntity getMeetingListNativeQuery(@RequestParam Double location @Operation(summary = "모임 검색 기능", description = "모임 검색 API") ResponseEntity getMeetingListBySearch(@RequestParam String keyword, @RequestParam(defaultValue = "1") int page); + @Operation(summary = "인기 모임", description = "인기 모임 API") + ResponseEntity getPopularMeeting(); + @Operation(summary = "회원 모임 참가 기능", description = "모임 참가 API") ResponseEntity enterMeeting(@PathVariable Long meetingId, @AuthenticationPrincipal UserDetailsImpl userDetails); diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/CareerDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/CareerDto.java new file mode 100644 index 0000000..18dbfd3 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/CareerDto.java @@ -0,0 +1,16 @@ +package com.sparta.moit.domain.meeting.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CareerDto { + private final Long careerId; + private final String careerName; + + @Builder + public CareerDto(Long careerId, String careerName) { + this.careerId = careerId; + this.careerName = careerName; + } +} \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/CareerResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/CareerResponseDto.java new file mode 100644 index 0000000..4b8b6ad --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/CareerResponseDto.java @@ -0,0 +1,25 @@ +package com.sparta.moit.domain.meeting.dto; + +import com.sparta.moit.domain.meeting.entity.Career; +import lombok.Builder; +import lombok.Getter; + + +@Getter +public class CareerResponseDto { + private final Long careerId; + private final String careerName; + + @Builder + public CareerResponseDto(Long careerId, String careerName) { + this.careerId = careerId; + this.careerName = careerName; + } + + public static CareerResponseDto fromEntity(Career career) { + return CareerResponseDto.builder() + .careerId(career.getId()) + .careerName(career.getCareerName()) + .build(); + } +} diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingRequestDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingRequestDto.java index 0788e77..b4741b0 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingRequestDto.java +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingRequestDto.java @@ -1,30 +1,60 @@ package com.sparta.moit.domain.meeting.dto; +import com.fasterxml.jackson.annotation.JsonFormat; import com.sparta.moit.domain.meeting.entity.Meeting; import com.sparta.moit.domain.member.entity.Member; +import com.sparta.moit.global.util.PointUtil; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import static com.sparta.moit.global.util.CareerMapper.createCareerResponseList; +import static com.sparta.moit.global.util.SkillMapper.createSkillResponseList; + +@Slf4j(topic = "CreateRequestDto") @Getter public class CreateMeetingRequestDto { @Schema(description = "미팅 제목", example = "[모각코] 석촌호수 카페 모집중!") + @NotNull(message = "모임 이름 작성은 필수입니다.") private String meetingName; + @Schema(description = "미팅 날짜", example = "2024-04-11") + @NotNull(message = "미팅 날짜 선택은 필수입니다.") private LocalDate meetingDate; + +// @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "Asia/Seoul") + @NotNull(message = "모임 시작 시간 선택은 필수입니다.") private LocalDateTime meetingStartTime; + + @NotNull(message = "모임 종료 시간 선택은 필수입니다.") + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") private LocalDateTime meetingEndTime; + @Schema(description = "미팅 예산", example = "10000") + @NotNull(message = "예산 입력은 필수입니다.") + @Min(value = 0, message = "0원 이상 입력해주세요.") + @Max(value = 1000000, message = "최대 100만원까지만 입력 가능합니다.") private Integer budget; + @Schema(description = "미팅 내용", example = "석촌호수 근처 카페에서 모각코 진행합니다! 관심있으신 분은 채팅 참여해주세요 :)") + @NotNull(message = "모임 내용 입력은 필수입니다.") private String contents; + @Schema(description = "총 인원", example = "8") + @NotNull(message = "총 인원 선택은 필수입니다.") private Short totalCount; + @Schema(description = "미팅 장소", example = "서울특별시 송파구 석촌동 172-24 석촌동 24-11 나인파크A 201호") + @NotNull(message = "모임 장소 선택은 필수입니다.") private String locationAddress; + @Schema(description = "미팅 위도", example = "37.50157") private Double locationLat; @Schema(description = "미팅 경도", example = "127.040157") @@ -33,17 +63,59 @@ public class CreateMeetingRequestDto { private String regionFirstName; @Schema(description = "시군구", example = "송파구") private String regionSecondName; + @Schema(description = "기술스택 ID 리스트", example = "[1, 2]") + @NotNull(message = "기술 스택 선택은 필수입니다.") private List skillIds; + @Schema(description = "경력 ID 리스트", example = "[1, 2]") + @NotNull(message = "경력 선택은 필수입니다.") private List careerIds; public Meeting toEntity(Member creator) { + /* 시간 관련 validation */ + if (meetingStartTime != null && meetingEndTime != null && meetingStartTime.isAfter(meetingEndTime)) { + throw new IllegalArgumentException("모임 시작 시간은 종료 시간보다 빨라야 합니다."); + } + List skillList = createSkillResponseList(skillIds); + + List careerList = createCareerResponseList(careerIds); + + return Meeting.builder() + .meetingName(this.meetingName) + .meetingDate(this.meetingDate) + .meetingStartTime(this.meetingStartTime.plusHours(9)) + .meetingEndTime(this.meetingEndTime.plusHours(9)) + .budget(this.budget) + .contents(this.contents) + .locationAddress(this.locationAddress) + .registeredCount((short) 1) + .totalCount(this.totalCount) + .locationLat(this.locationLat) + .locationLng(this.locationLng) + .locationPosition(PointUtil.createPointFromLngLat(locationLng, locationLat)) + .regionFirstName(this.regionFirstName) + .regionSecondName(this.regionSecondName) + .creator(creator) + .skillList(skillList) + .careerList(careerList) + .build(); + } + + public Meeting toEntityArray(Member creator) { + /* 시간 관련 validation */ + if (meetingStartTime != null && meetingEndTime != null && meetingStartTime.isAfter(meetingEndTime)) { + throw new IllegalArgumentException("모임 시작 시간은 종료 시간보다 빨라야 합니다."); + } + + Long[] skillArray = skillIds.toArray(new Long[0]); + Long[] careerArray = careerIds.toArray(new Long[0]); + return Meeting.builder() .meetingName(this.meetingName) .meetingDate(this.meetingDate) - .meetingStartTime(this.meetingStartTime) - .meetingEndTime(this.meetingEndTime) + .meetingStartTime(this.meetingStartTime.plusHours(9)) + .meetingEndTime(this.meetingEndTime.plusHours(9)) .budget(this.budget) .contents(this.contents) .locationAddress(this.locationAddress) @@ -51,9 +123,13 @@ public Meeting toEntity(Member creator) { .totalCount(this.totalCount) .locationLat(this.locationLat) .locationLng(this.locationLng) + .locationPosition(PointUtil.createPointFromLngLat(locationLng, locationLat)) .regionFirstName(this.regionFirstName) .regionSecondName(this.regionSecondName) .creator(creator) + .skillIdList(skillArray) + .careerIdList(careerArray) .build(); } + } diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingResponseDto.java deleted file mode 100644 index cc0cc24..0000000 --- a/src/main/java/com/sparta/moit/domain/meeting/dto/CreateMeetingResponseDto.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.sparta.moit.domain.meeting.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.sparta.moit.domain.meeting.entity.*; -import lombok.Builder; -import lombok.Getter; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.stream.Collectors; - -@Getter -public class CreateMeetingResponseDto { - private final Long meetingId; - private final String meetingName; - private final LocalDate meetingDate; - @JsonFormat(pattern = "HH:mm") - private final ZonedDateTime meetingStartTime; - @JsonFormat(pattern = "HH:mm") - private final ZonedDateTime meetingEndTime; - private final String locationAddress; - private final Integer budget; - private final String contents; - private final Short registeredCount; - private final Short totalCount; - private final Double locationLat; - private final Double locationLng; - private final String regionFirstName; - private final String regionSecondName; - private final List skillList; - private final List careerList; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private final ZonedDateTime createdAt; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private final ZonedDateTime modifiedAt; - - @Builder - public CreateMeetingResponseDto(Long meetingId, String meetingName, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String locationAddress, Integer budget, String contents, Short registeredCount, Short totalCount, Double locationLat, Double locationLng, String regionFirstName, String regionSecondName, List skillList, List careerList, LocalDateTime createdAt, LocalDateTime modifiedAt) { - this.meetingId = meetingId; - this.meetingName = meetingName; - this.meetingDate = meetingDate; - this.meetingStartTime = meetingStartTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.meetingEndTime = meetingEndTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.budget = budget; - this.contents = contents; - this.locationAddress = locationAddress; - this.registeredCount = registeredCount; - this.totalCount = totalCount; - this.locationLat = locationLat; - this.locationLng = locationLng; - this.regionFirstName = regionFirstName; - this.regionSecondName = regionSecondName; - this.skillList = skillList; - this.careerList = careerList; - this.createdAt = createdAt.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.modifiedAt = modifiedAt.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - } - - public static CreateMeetingResponseDto fromEntity(Meeting meeting) { - List skillList = meeting.getSkills().stream() - .map(MeetingSkill::getSkill) - .collect(Collectors.toList()); - - List careerList = meeting.getCareers().stream() - .map(MeetingCareer::getCareer) - .collect(Collectors.toList()); - - return CreateMeetingResponseDto.builder() - .meetingId(meeting.getId()) - .meetingName(meeting.getMeetingName()) - .meetingDate(meeting.getMeetingDate()) - .meetingStartTime(meeting.getMeetingStartTime()) - .meetingEndTime(meeting.getMeetingEndTime()) - .budget(meeting.getBudget()) - .contents(meeting.getContents()) - .registeredCount(meeting.getRegisteredCount()) - .totalCount(meeting.getTotalCount()) - .locationAddress(meeting.getLocationAddress()) - .locationLat(meeting.getLocationLat()) - .locationLng(meeting.getLocationLng()) - .regionFirstName(meeting.getRegionFirstName()) - .regionSecondName(meeting.getRegionSecondName()) - .skillList(skillList) - .careerList(careerList) - .createdAt(meeting.getCreatedAt()) - .modifiedAt(meeting.getModifiedAt()) - .build(); - } -} diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingArrayResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingArrayResponseDto.java new file mode 100644 index 0000000..dc7029f --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingArrayResponseDto.java @@ -0,0 +1,84 @@ +package com.sparta.moit.domain.meeting.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.sparta.moit.domain.meeting.entity.Meeting; +import com.sparta.moit.domain.meeting.entity.MeetingStatusEnum; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static com.sparta.moit.global.util.CareerMapper.createCareerResponseList; +import static com.sparta.moit.global.util.SkillMapper.createSkillResponseList; + +@Getter +@NoArgsConstructor +public class GetMeetingArrayResponseDto { + private Long meetingId; + private String meetingName; + private Short registeredCount; + private Short totalCount; + private LocalDate meetingDate; + private Double locationLat; + private Double locationLng; + private String locationAddress; + @JsonFormat(pattern = "HH:mm") // 타임존 아시아 있었음 + private LocalDateTime meetingStartTime; + @JsonFormat(pattern = "HH:mm") + private LocalDateTime meetingEndTime; + private List skillList; + private List careerList; + private MeetingStatusEnum status; + + @Builder + GetMeetingArrayResponseDto( + Long meetingId, String meetingName, Short registeredCount, Short totalCount, Double locationLat, + Double locationLng, List skillList, List careerList, LocalDate meetingDate, LocalDateTime meetingStartTime, + LocalDateTime meetingEndTime, String locationAddress, MeetingStatusEnum status) { + this.meetingId = meetingId; + this.meetingName = meetingName; + this.registeredCount = registeredCount; + this.totalCount = totalCount; + this.meetingDate = meetingDate; + this.locationLat = locationLat; + this.locationLng = locationLng; + this.meetingStartTime = meetingStartTime; + this.meetingEndTime = meetingEndTime; + this.locationAddress = locationAddress; + this.skillList = skillList; + this.careerList = careerList; + this.status = status; + } + + public static GetMeetingArrayResponseDto fromEntity(Meeting meeting) { + List skillList = Collections.emptyList(); + List careerList = Collections.emptyList(); + if (meeting.getSkillIdList() != null) { + skillList = createSkillResponseList(meeting.getSkillIdList()); + } + if (meeting.getCareerIdList() != null) { + careerList = createCareerResponseList(meeting.getCareerIdList()); + } + + return GetMeetingArrayResponseDto.builder() + .meetingId(meeting.getId()) + .meetingName(meeting.getMeetingName()) + .registeredCount(meeting.getRegisteredCount()) + .totalCount(meeting.getTotalCount()) + .locationLat(meeting.getLocationLat()) + .locationLng(meeting.getLocationLng()) + .meetingDate(meeting.getMeetingDate()) + .meetingStartTime(meeting.getMeetingStartTime()) + .meetingEndTime(meeting.getMeetingEndTime()) + .locationAddress(meeting.getLocationAddress()) + .skillList(skillList) + .careerList(careerList) + .status(meeting.getStatus()) + .build(); + } + +} diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingDetailResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingDetailResponseDto.java index 65f99c9..c5ac70e 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingDetailResponseDto.java +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/GetMeetingDetailResponseDto.java @@ -2,15 +2,19 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.sparta.moit.domain.meeting.entity.Meeting; +import com.sparta.moit.domain.meeting.entity.MeetingStatusEnum; import lombok.Builder; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; import java.util.List; +import static com.sparta.moit.global.util.CareerMapper.mapCareerIdsToNames; +import static com.sparta.moit.global.util.SkillMapper.mapSkillIdsToNames; + +@Slf4j(topic = "GetMeetingDetailResponseDto") @Getter public class GetMeetingDetailResponseDto { @@ -23,9 +27,9 @@ public class GetMeetingDetailResponseDto { private final LocalDate meetingDate; @JsonFormat(pattern = "HH:mm") - private final ZonedDateTime meetingStartTime; + private final LocalDateTime meetingStartTime; @JsonFormat(pattern = "HH:mm") - private final ZonedDateTime meetingEndTime; + private final LocalDateTime meetingEndTime; private final String locationAddress; @@ -36,9 +40,11 @@ public class GetMeetingDetailResponseDto { private final Double locationLat; private final Double locationLng; private final boolean isJoin; + private final MeetingStatusEnum status; + private final boolean isBookmarked; @Builder - public GetMeetingDetailResponseDto(Long meetingId, String meetingName, String creatorName, String creatorEmail, List careerNameList, List skillNameList, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String locationAddress, Short registeredCount, Short totalCount, Integer budget, String contents, Double locationLat, Double locationLng, boolean isJoin) { + public GetMeetingDetailResponseDto(Long meetingId, String meetingName, String creatorName, String creatorEmail, List careerNameList, List skillNameList, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String locationAddress, Short registeredCount, Short totalCount, Integer budget, String contents, Double locationLat, Double locationLng, boolean isJoin, MeetingStatusEnum status, boolean isBookmarked) { this.meetingId = meetingId; this.meetingName = meetingName; this.creatorName = creatorName; @@ -46,8 +52,8 @@ public GetMeetingDetailResponseDto(Long meetingId, String meetingName, String cr this.careerNameList = careerNameList; this.skillNameList = skillNameList; this.meetingDate = meetingDate; - this.meetingStartTime = meetingStartTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.meetingEndTime = meetingEndTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); + this.meetingStartTime = meetingStartTime; + this.meetingEndTime = meetingEndTime; this.locationAddress = locationAddress; this.registeredCount = registeredCount; this.totalCount = totalCount; @@ -56,9 +62,15 @@ public GetMeetingDetailResponseDto(Long meetingId, String meetingName, String cr this.locationLat = locationLat; this.locationLng = locationLng; this.isJoin = isJoin; + this.status = status; + this.isBookmarked = isBookmarked; } - public static GetMeetingDetailResponseDto fromEntity(Meeting meeting, List careerNameList, List skillNameList, boolean isJoin) { + public static GetMeetingDetailResponseDto fromEntity(Meeting meeting, boolean isJoin, boolean isBookmarked) { + List careerNameList = mapCareerIdsToNames(meeting.getCareerIdList()); + + List skillNameList = mapSkillIdsToNames(meeting.getSkillIdList()); + return GetMeetingDetailResponseDto.builder() .meetingId(meeting.getId()) .meetingName(meeting.getMeetingName()) @@ -77,6 +89,8 @@ public static GetMeetingDetailResponseDto fromEntity(Meeting meeting, List skillList; - private List careerList; + private LocalDateTime meetingEndTime; + private List skillList; + private List careerList; + private MeetingStatusEnum status; @Builder - public GetMeetingResponseDto(Long meetingId, String meetingName, Short registeredCount, Short totalCount, Double locationLat, Double locationLng, List skillList, List careerList, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String locationAddress) { + public GetMeetingResponseDto(Long meetingId, String meetingName, Short registeredCount, Short totalCount, Double locationLat, Double locationLng, List skillList, List careerList, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String locationAddress, MeetingStatusEnum status) { this.meetingId = meetingId; this.meetingName = meetingName; this.registeredCount = registeredCount; this.totalCount = totalCount; - this.meetingDate = meetingStartTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); + this.meetingDate = meetingDate; this.locationLat = locationLat; this.locationLng = locationLng; - this.meetingStartTime = meetingStartTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.meetingEndTime = meetingEndTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); + this.meetingStartTime = meetingStartTime; + this.meetingEndTime = meetingEndTime; this.locationAddress = locationAddress; this.skillList = skillList; this.careerList = careerList; + this.status = status; } public static GetMeetingResponseDto fromEntity(Meeting meeting){ - List skillList = meeting.getSkills().stream() - .map(MeetingSkill::getSkill) - .collect(Collectors.toList()); - List careerList = meeting.getCareers().stream() - .map(MeetingCareer::getCareer) - .collect(Collectors.toList()); - return GetMeetingResponseDto.builder() .meetingId(meeting.getId()) .meetingName(meeting.getMeetingName()) @@ -66,11 +58,13 @@ public static GetMeetingResponseDto fromEntity(Meeting meeting){ .totalCount(meeting.getTotalCount()) .locationLat(meeting.getLocationLat()) .locationLng(meeting.getLocationLng()) + .meetingDate(meeting.getMeetingDate()) .meetingStartTime(meeting.getMeetingStartTime()) .meetingEndTime(meeting.getMeetingEndTime()) .locationAddress(meeting.getLocationAddress()) - .skillList(skillList) - .careerList(careerList) + .skillList(meeting.getSkillList()) + .careerList(meeting.getCareerList()) + .status(meeting.getStatus()) .build(); } } diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/GetPopularResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/GetPopularResponseDto.java new file mode 100644 index 0000000..c9fe7e1 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/GetPopularResponseDto.java @@ -0,0 +1,66 @@ +package com.sparta.moit.domain.meeting.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.sparta.moit.domain.meeting.entity.Meeting; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Getter +@NoArgsConstructor() +public class GetPopularResponseDto { + private Long meetingId; + private String meetingName; + private LocalDate meetingDate; + private String locationAddress; + @JsonFormat(pattern = "HH:mm") + private LocalDateTime meetingStartTime; + @JsonFormat(pattern = "HH:mm") + private LocalDateTime meetingEndTime; + + @Builder + public GetPopularResponseDto(Long meetingId, String meetingName, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String locationAddress) { + this.meetingId = meetingId; + this.meetingName = meetingName; + this.meetingDate = meetingDate; + this.meetingStartTime = meetingStartTime; + this.meetingEndTime = meetingEndTime; + this.locationAddress = locationAddress; + } + + public static GetPopularResponseDto fromEntity(Meeting meeting){ + String formattedLocationAddress = formatLocationAddress(meeting.getLocationAddress()); + + return GetPopularResponseDto.builder() + .meetingId(meeting.getId()) + .meetingName(meeting.getMeetingName()) + .meetingDate(meeting.getMeetingDate()) + .meetingStartTime(meeting.getMeetingStartTime().minusHours(9)) + .meetingEndTime(meeting.getMeetingEndTime().minusHours(9)) + .locationAddress(formattedLocationAddress) + .build(); + } + + /* locationAddress 포맷 변경 메서드 */ + private static String formatLocationAddress(String locationAddress) { + String[] parts = locationAddress.split("\\s+"); + StringBuilder formattedAddress = new StringBuilder(); + for (int i = 0; i < 2; i++) { + formattedAddress.append(parts[i]); + if (i == 0) { + formattedAddress.append(" "); + } + } + return formattedAddress.toString(); + } + + /* meetingDate 형식 변경 메서드 */ + private static String formatMeetingDate(LocalDate meetingDate) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M월 d일 E"); + return meetingDate.format(formatter); + } +} diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/RegionFirstResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/RegionFirstResponseDto.java index 521afc9..cc85acb 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/dto/RegionFirstResponseDto.java +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/RegionFirstResponseDto.java @@ -6,10 +6,10 @@ @Getter public class RegionFirstResponseDto { - private Short regionFirstId; + private Long regionFirstId; private String regionFirstName; @Builder - public RegionFirstResponseDto(Short regionFirstId, String regionFirstName) { + public RegionFirstResponseDto(Long regionFirstId, String regionFirstName) { this.regionFirstId = regionFirstId; this.regionFirstName = regionFirstName; } diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/RegionIntegratedResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/RegionIntegratedResponseDto.java new file mode 100644 index 0000000..4eb3e03 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/RegionIntegratedResponseDto.java @@ -0,0 +1,31 @@ +package com.sparta.moit.domain.meeting.dto; + +import com.sparta.moit.domain.meeting.entity.RegionFirst; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +@Getter +public class RegionIntegratedResponseDto { + private Long regionFirstId; + private String regionFirstName; + private List secondResponseDtos; + + @Builder + public RegionIntegratedResponseDto(Long regionFirstId, String regionFirstName, List secondResponseDtos) { + this.regionFirstId = regionFirstId; + this.regionFirstName = regionFirstName; + this.secondResponseDtos = secondResponseDtos; + } + + public static RegionIntegratedResponseDto fromEntity(RegionFirst regionFirst){ + List seconds = regionFirst.getRegionSeconds() + .stream() + .map(RegionSecondResponseDto::fromEntity).toList(); + return RegionIntegratedResponseDto.builder() + .regionFirstId(regionFirst.getRegionFirstId()) + .regionFirstName(regionFirst.getRegionFirstName()) + .secondResponseDtos(seconds) + .build(); + } +} diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/RegionSecondResponseDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/RegionSecondResponseDto.java index 379f12a..7643096 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/dto/RegionSecondResponseDto.java +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/RegionSecondResponseDto.java @@ -6,14 +6,14 @@ @Getter public class RegionSecondResponseDto { - private Short regionSecondId; + private Long regionSecondId; private String regionSecondName; private Double regionLat; private Double regionLng; @Builder - public RegionSecondResponseDto(Short regionSecondId, String regionSecondName, Double regionLat, Double regionLng) { + public RegionSecondResponseDto(Long regionSecondId, String regionSecondName, Double regionLat, Double regionLng) { this.regionSecondId = regionSecondId; this.regionSecondName = regionSecondName; this.regionLat = regionLat; diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/SkillDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/SkillDto.java new file mode 100644 index 0000000..0a1d9ae --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/SkillDto.java @@ -0,0 +1,16 @@ +package com.sparta.moit.domain.meeting.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SkillDto { + private final Long skillId; + private final String skillName; + + @Builder + public SkillDto(Long skillId, String skillName) { + this.skillId = skillId; + this.skillName = skillName; + } +} \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/domain/meeting/dto/UpdateMeetingRequestDto.java b/src/main/java/com/sparta/moit/domain/meeting/dto/UpdateMeetingRequestDto.java index ffd8006..4aa2da6 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/dto/UpdateMeetingRequestDto.java +++ b/src/main/java/com/sparta/moit/domain/meeting/dto/UpdateMeetingRequestDto.java @@ -3,6 +3,9 @@ import com.sparta.moit.domain.meeting.entity.Meeting; import com.sparta.moit.domain.member.entity.Member; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import java.time.LocalDate; @@ -12,26 +15,44 @@ @Getter public class UpdateMeetingRequestDto { @Schema(description = "미팅 제목", example = "[모각코] 석촌호수 카페 모집중!") + @NotNull(message = "모임 이름 작성은 필수입니다.") private String meetingName; + @Schema(description = "미팅 예산", example = "10000") + @NotNull(message = "예산 입력은 필수입니다.") + @Min(value = 0, message = "0원 이상 입력해주세요.") + @Max(value = 1000000, message = "최대 100만원까지만 입력 가능합니다.") private Integer budget; + @Schema(description = "미팅 내용", example = "석촌호수 근처 카페에서 모각코 진행합니다! 관심있으신 분은 채팅 참여해주세요 :)") + @NotNull(message = "모임 내용 입력은 필수입니다.") private String contents; + @Schema(description = "총 인원", example = "8") + @NotNull(message = "총 인원 선택은 필수입니다.") private Short totalCount; + @Schema(description = "미팅 장소", example = "서울특별시 송파구 석촌동 172-24 석촌동 24-11 나인파크A 201호") + @NotNull(message = "모임 장소 선택은 필수입니다.") private String locationAddress; + @Schema(description = "미팅 위도", example = "37.50157") private Double locationLat; + @Schema(description = "미팅 경도", example = "127.040157") private Double locationLng; + @Schema(description = "시-도", example = "서울특별시") private String regionFirstName; @Schema(description = "시군구", example = "송파구") private String regionSecondName; + @Schema(description = "기술스택 ID 리스트", example = "[3, 4]") + @NotNull(message = "기술 스택 선택은 필수입니다.") private List skillIds; + @Schema(description = "경력 ID 리스트", example = "[3, 4]") + @NotNull(message = "경력 선택은 필수입니다.") private List careerIds; public Meeting toEntity(Member member) { diff --git a/src/main/java/com/sparta/moit/domain/meeting/entity/Meeting.java b/src/main/java/com/sparta/moit/domain/meeting/entity/Meeting.java index 50b2f9b..059a063 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/entity/Meeting.java +++ b/src/main/java/com/sparta/moit/domain/meeting/entity/Meeting.java @@ -2,17 +2,26 @@ import com.fasterxml.jackson.annotation.JsonIgnore; +import com.sparta.moit.domain.meeting.dto.CareerResponseDto; +import com.sparta.moit.domain.meeting.dto.SkillResponseDto; import com.sparta.moit.domain.meeting.dto.UpdateMeetingRequestDto; import com.sparta.moit.domain.member.entity.Member; import com.sparta.moit.global.common.entity.Timestamped; +import com.sparta.moit.global.util.PointUtil; +import io.hypersistence.utils.hibernate.type.json.JsonType; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.Type; +import org.locationtech.jts.geom.Point; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import static com.sparta.moit.global.util.CareerMapper.createCareerResponseList; +import static com.sparta.moit.global.util.SkillMapper.createSkillResponseList; + @Entity(name = "meeting") @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -56,6 +65,9 @@ public class Meeting extends Timestamped { @Column(name = "location_lng") private Double locationLng; + @Column(columnDefinition = "geography(Point,4326)") + private Point locationPosition; + @Column(name = "region_first_name") private String regionFirstName; @@ -74,18 +86,27 @@ public class Meeting extends Timestamped { @JsonIgnore private List meetingMembers = new ArrayList<>(); - @JsonIgnore - @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - private List skills = new ArrayList<>(); + @Type(JsonType.class) + @Column(name = "skill_list",columnDefinition = "jsonb") + private List skillList = new ArrayList<>(); + + @Type(JsonType.class) + @Column(name = "career_list",columnDefinition = "jsonb") + private List careerList = new ArrayList<>(); + + @Column(name = "career_id_list", columnDefinition = "bigint[]") + private Long[] careerIdList; + + @Column(name = "skill_id_list", columnDefinition = "bigint[]") + private Long[] skillIdList; - @JsonIgnore - @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - private List careers = new ArrayList<>(); @Builder - public Meeting(Long id, String meetingName, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, Integer budget, - String locationAddress, String contents, Short registeredCount, Short totalCount, - Double locationLat, Double locationLng, String regionFirstName, String regionSecondName, MeetingStatusEnum status, Member creator, List meetingMembers) { + public Meeting(Long id, String meetingName, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, + Integer budget, String locationAddress, String contents, Short registeredCount, Short totalCount, + Double locationLat, Double locationLng, Point locationPosition, String regionFirstName, String regionSecondName, MeetingStatusEnum status, + Member creator, List skillList, List careerList, Long[] careerIdList, + Long[] skillIdList) { this.id = id; this.meetingName = meetingName; this.meetingDate = meetingDate; @@ -98,14 +119,39 @@ public Meeting(Long id, String meetingName, LocalDate meetingDate, LocalDateTime this.totalCount = totalCount; this.locationLat = locationLat; this.locationLng = locationLng; + this.locationPosition = locationPosition; this.regionFirstName = regionFirstName; this.regionSecondName = regionSecondName; this.status = MeetingStatusEnum.OPEN; this.creator = creator; - this.meetingMembers = new ArrayList<>(); + this.skillList = skillList; + this.careerList = careerList; + this.skillIdList = skillIdList; + this.careerIdList = careerIdList; } public void updateMeeting(UpdateMeetingRequestDto requestDto) { + List skillList = createSkillResponseList(requestDto.getSkillIds()); + + List careerList = createCareerResponseList(requestDto.getCareerIds()); + + this.meetingName = requestDto.getMeetingName(); + this.budget = requestDto.getBudget(); + this.locationAddress = requestDto.getLocationAddress(); + this.contents = requestDto.getContents(); + this.totalCount = requestDto.getTotalCount(); + this.locationLat = requestDto.getLocationLat(); + this.locationLng = requestDto.getLocationLng(); + this.locationPosition = PointUtil.createPointFromLngLat(requestDto.getLocationLng(), requestDto.getLocationLat()); + this.regionFirstName = requestDto.getRegionFirstName(); + this.regionSecondName = requestDto.getRegionSecondName(); + this.skillList = skillList; + this.careerList = careerList; + } + public void updateMeetingArray(UpdateMeetingRequestDto requestDto) { + Long[] skillArray = requestDto.getSkillIds().toArray(new Long[0]); + Long[] careerArray = requestDto.getCareerIds().toArray(new Long[0]); + this.meetingName = requestDto.getMeetingName(); this.budget = requestDto.getBudget(); this.locationAddress = requestDto.getLocationAddress(); @@ -113,8 +159,11 @@ public void updateMeeting(UpdateMeetingRequestDto requestDto) { this.totalCount = requestDto.getTotalCount(); this.locationLat = requestDto.getLocationLat(); this.locationLng = requestDto.getLocationLng(); + this.locationPosition = PointUtil.createPointFromLngLat(requestDto.getLocationLng(), requestDto.getLocationLat()); this.regionFirstName = requestDto.getRegionFirstName(); this.regionSecondName = requestDto.getRegionSecondName(); + this.skillIdList = skillArray; + this.careerIdList = careerArray; } public Short incrementRegisteredCount() { diff --git a/src/main/java/com/sparta/moit/domain/meeting/entity/RegionFirst.java b/src/main/java/com/sparta/moit/domain/meeting/entity/RegionFirst.java index 41f1b8d..71fd59f 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/entity/RegionFirst.java +++ b/src/main/java/com/sparta/moit/domain/meeting/entity/RegionFirst.java @@ -1,10 +1,14 @@ package com.sparta.moit.domain.meeting.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import lombok.*; +import java.util.List; + @Entity(name="region_first") @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -14,8 +18,12 @@ public class RegionFirst { @Column(name = "region_first_id") @Id - private Short regionFirstId; + private Long regionFirstId; @Column(name = "region_first_name") private String regionFirstName; + + @OneToMany(mappedBy = "regionFirst") + @JsonManagedReference + private List regionSeconds; } diff --git a/src/main/java/com/sparta/moit/domain/meeting/entity/RegionSecond.java b/src/main/java/com/sparta/moit/domain/meeting/entity/RegionSecond.java index c936e19..63a0c18 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/entity/RegionSecond.java +++ b/src/main/java/com/sparta/moit/domain/meeting/entity/RegionSecond.java @@ -13,7 +13,7 @@ public class RegionSecond { @Column(name = "region_second_id") @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Short regionSecondId; + private Long regionSecondId; private Double regionLat; diff --git a/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepository.java b/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepository.java index 8527503..8c4bfdf 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepository.java +++ b/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepository.java @@ -5,6 +5,7 @@ import com.sparta.moit.domain.member.entity.Member; 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; @@ -13,9 +14,64 @@ public interface MeetingRepository extends JpaRepository, Meeting Optional findByIdAndCreator(Long meetingId, Member member); - int countByCreator(Member creator); int countByCreatorAndStatusNot(Member creator, MeetingStatusEnum status); + @Query("SELECT m FROM meeting m WHERE m.creator.id = :memberId AND m.status != :status " + + "ORDER BY CASE WHEN m.status = 'OPEN' OR m.status = 'FULL' THEN m.meetingDate END ASC, " + + "CASE WHEN m.status = 'COMPLETE' THEN m.meetingDate END DESC") + List findMeetingsByCreatorIdAndStatusNot(Long memberId, MeetingStatusEnum status); + + @Query("SELECT m FROM meeting m WHERE m.creator.id = :memberId AND m.status = :status " + + "ORDER BY m.meetingDate DESC ") + List findMeetingsByCreatorIdAndStatus(Long memberId, MeetingStatusEnum status); + + @Query(value = "SELECT m.*, " + + "ST_Distance( CAST (ST_SetSRID(ST_MakePoint(:locationLng, :locationLat), 4326) AS geography), m.location_position) as dist " + + "FROM meeting m " + + "WHERE " + + " ST_Dwithin( CAST (ST_SetSRID(ST_MakePoint(:locationLng, :locationLat), 4326) AS geography), m.location_position, 5000) " + + " AND ((:skillIdsStr IS NULL OR EXISTS (" + + " SELECT 1 FROM jsonb_array_elements(m.skill_list) AS skill_json " + + " WHERE CAST(skill_json->>'skillId' AS TEXT) = ANY(string_to_array(:skillIdsStr, ',')) " + + " )) " + + " OR (:careerIdsStr IS NULL OR EXISTS (" + + " SELECT 1 FROM jsonb_array_elements(m.career_list) AS career_json " + + " WHERE CAST(career_json->>'careerId' AS TEXT) = ANY(string_to_array(:careerIdsStr, ',')) " + + " ))) " + + " AND m.status <> 'DELETE' " + + " AND m.status <> 'COMPLETE' "+ + " ORDER BY dist asc " + + "LIMIT :pageSize " + + "OFFSET :offset", nativeQuery = true) + List findMeetingST_Dwithin(@Param("locationLng") Double locationLng, + @Param("locationLat") Double locationLat, + @Param("skillIdsStr") String skillIdsStr, + @Param("careerIdsStr") String careerIdsStr, + @Param("pageSize") int pageSize, + @Param("offset") int offset); + + @Query(value = "SELECT m.*, " + + "ST_Distance( CAST (ST_SetSRID(ST_MakePoint(:locationLng, :locationLat), 4326) AS geography), m.location_position) as dist " + + "FROM meeting m " + + "WHERE " + + " ST_Dwithin( CAST (ST_SetSRID(ST_MakePoint(:locationLng, :locationLat), 4326) AS geography), m.location_position, 5000) " + + " AND (" + + " (:skillIdsStr IS NULL OR m.skill_id_list && CAST(string_to_array(:skillIdsStr, ',') AS bigint[])) " + + " OR (:careerIdsStr IS NULL OR m.career_id_list && CAST(string_to_array(:careerIdsStr, ',') AS bigint[])) " + + ")" + + " AND m.status <> 'DELETE' " + + " AND m.status <> 'COMPLETE' "+ + " ORDER BY dist asc " + + "LIMIT :pageSize " + + "OFFSET :offset", nativeQuery = true) + List findMeetingST_Dwithin_array(@Param("locationLng") Double locationLng, + @Param("locationLat") Double locationLat, + @Param("skillIdsStr") String skillIdsStr, + @Param("careerIdsStr") String careerIdsStr, + @Param("pageSize") int pageSize, + @Param("offset") int offset); + + @Query(value = "SELECT * FROM meeting " + "ORDER BY ST_DISTANCE_SPHERE(point(:locationLng, :locationLat), point(location_lng, location_lat)) " + "LIMIT :limit OFFSET :page", @@ -46,4 +102,6 @@ public interface MeetingRepository extends JpaRepository, Meeting + "LIMIT :limit OFFSET :page", nativeQuery = true) List getMeetingsWithSkillAndCareer(Double locationLat, Double locationLng, List skillId, List careerId, int limit, int page); + + } diff --git a/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryCustom.java b/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryCustom.java index fb126c0..70dc029 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryCustom.java +++ b/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryCustom.java @@ -10,10 +10,6 @@ public interface MeetingRepositoryCustom { - List findCareerNameList(Long meetingId); - - List findSkillNameList(Long meetingId); - Slice getMeetingSlice(Double locationLat, Double locationLng, List skillId, List careerId, Pageable pageable); Slice findByKeyword(String keyword, Pageable pageable); @@ -23,4 +19,8 @@ public interface MeetingRepositoryCustom { List findMeetingsByMember(Long memberId); List findAllIncompleteMeetingsForHour(); + + List getPopularMeetings(); + + List findHeldMeetingsByCreatorId(Long memberId); } diff --git a/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryImpl.java b/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryImpl.java index 9379fd8..498a806 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryImpl.java +++ b/src/main/java/com/sparta/moit/domain/meeting/repository/MeetingRepositoryImpl.java @@ -1,32 +1,33 @@ package com.sparta.moit.domain.meeting.repository; +import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.ComparableExpressionBase; -import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import com.sparta.moit.domain.meeting.dto.GetMyPageDto; import com.sparta.moit.domain.meeting.entity.Meeting; import com.sparta.moit.domain.meeting.entity.MeetingStatusEnum; import com.sparta.moit.domain.meeting.entity.QMeeting; -import com.sparta.moit.domain.meeting.entity.QMeetingMember; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; +import static com.sparta.moit.domain.bookmark.entity.QBookMark.bookMark; import static com.sparta.moit.domain.meeting.entity.QCareer.career; import static com.sparta.moit.domain.meeting.entity.QMeeting.meeting; -import static com.sparta.moit.domain.meeting.entity.QMeetingCareer.meetingCareer; import static com.sparta.moit.domain.meeting.entity.QMeetingMember.meetingMember; import static com.sparta.moit.domain.meeting.entity.QMeetingSkill.meetingSkill; -import static com.sparta.moit.domain.meeting.entity.QSkill.skill; -import static org.hibernate.query.results.Builders.fetch; - +@Slf4j(topic = "스케줄러") @RequiredArgsConstructor public class MeetingRepositoryImpl implements MeetingRepositoryCustom { private final JPAQueryFactory queryFactory; @@ -42,7 +43,7 @@ public List getMyPage(Long memberId, MeetingStatusEnum status) { .from(meetingMember) .leftJoin(meetingMember.meeting, meeting) .where(meetingMember.member.id.eq(memberId) - .and(meeting.status.ne(MeetingStatusEnum.DELETE))) // status != DELETE 조건 추가 + .and(meeting.status.eq(MeetingStatusEnum.COMPLETE))) /*완료인 경우만 조회*/ .fetch(); } @@ -56,6 +57,7 @@ public List findMeetingsByMember(Long memberId) { meetingMember.member.id.eq(memberId), isOpenOrFull() ) + .orderBy(meeting.meetingDate.asc()) .fetch(); return response; } @@ -66,8 +68,8 @@ public Slice getMeetingSlice(Double locationLat, Double locationLng, Li List meetingList = queryFactory .selectFrom(meeting) .distinct() - .leftJoin(meeting.skills, meetingSkill) - .leftJoin(meeting.careers, meetingCareer) +// .leftJoin(meeting.skills, meetingSkill) +// .leftJoin(meeting.careers, meetingCareer) .where( skillEq(skillId), careerEq(careerId), @@ -86,52 +88,81 @@ public Slice getMeetingSlice(Double locationLat, Double locationLng, Li /* 검색 */ @Override public Slice findByKeyword(String keyword, Pageable pageable) { - List meetingList = queryFactory - .selectFrom(meeting) - .where( - titleLike(keyword) - .or(addressLike(keyword)) - .or(contentLike(keyword)), - isOpenOrFull() - ) + QMeeting subMeeting = new QMeeting("subMeeting"); + + List meetings = queryFactory.selectFrom(meeting) + .where(meeting.id.in( + JPAExpressions.selectDistinct(subMeeting.id) + .from(subMeeting) + .where( + titleLike(keyword) + .or(addressLike(keyword)) + .or(contentLike(keyword)), + isOpenOrFull() + ) + )) .orderBy( meeting.meetingDate.asc(), - meeting.registeredCount.desc() + meeting.registeredCount.desc(), + meeting.id.desc() ) - .offset(pageable.getOffset()) .limit(pageable.getPageSize() + 1) + .offset(pageable.getOffset()) .fetch(); - return new SliceImpl<>(meetingList, pageable, hasNextPage(meetingList, pageable.getPageSize())); + return new SliceImpl<>(meetings, pageable, hasNextPage(meetings, pageable.getPageSize())); } @Override - public List findCareerNameList(Long meetingId) { - return queryFactory - .select(career.careerName) - .from(meetingCareer) - .join(meetingCareer.career, career) - .where(meetingCareer.meeting.id.eq(meetingId)) + public List findAllIncompleteMeetingsForHour() { + LocalDateTime oneHourAgo = LocalDateTime.now().plusHours(8); + LocalDateTime now = LocalDateTime.now().plusHours(9); + LocalTime oneHourAgoTime = oneHourAgo.toLocalTime(); + LocalTime nowTime = now.toLocalTime(); + + return queryFactory.selectFrom(meeting) + .where(isOpenOrFull()) + .where(meeting.meetingDate.eq(now.toLocalDate())) + /* meetingDate가 오늘이고, meetingEndTime이 1시간 전부터 현재까지인 회의 */ + .where(meeting.meetingEndTime.hour().between( + oneHourAgoTime.getHour(), nowTime.getHour() + ) + ) .fetch(); } @Override - public List findSkillNameList(Long meetingId) { - return queryFactory - .select(skill.skillName) - .from(meetingSkill) - .join(meetingSkill.skill, skill) - .where(meetingSkill.meeting.id.eq(meetingId)) + public List getPopularMeetings() { + List ids= queryFactory.select(bookMark.meeting.id) + .from(bookMark) + .join(bookMark.meeting, meeting) + .where(meeting.status.eq(MeetingStatusEnum.OPEN).or(meeting.status.eq(MeetingStatusEnum.FULL))) + .groupBy(bookMark.meeting.id) + .orderBy(bookMark.meeting.count().desc()) + .limit(5) + .fetch(); + + return queryFactory.selectFrom(meeting) + .where(meeting.id.in(ids)) .fetch(); } - @Override - public List findAllIncompleteMeetingsForHour() { - LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1); + public List findHeldMeetingsByCreatorId(Long memberId) { + QMeeting m = QMeeting.meeting; - return queryFactory.selectFrom(meeting) - .where(isOpenOrFull()) - /* now() - 1hr <= meetingEndTime < now() */ - .where(meeting.meetingStartTime.between(oneHourAgo, LocalDateTime.now())) + BooleanExpression whereClause = m.creator.id.eq(memberId) + .and(m.status.ne(MeetingStatusEnum.DELETE)); + + List> orderBy = new ArrayList<>(); + orderBy.add(new CaseBuilder() + .when(m.status.eq(MeetingStatusEnum.OPEN).or(m.status.eq(MeetingStatusEnum.FULL))) + .then(1) + .otherwise(2).asc()); + orderBy.add(m.meetingDate.asc()); + + return queryFactory + .selectFrom(m) + .where(whereClause) + .orderBy(orderBy.toArray(new OrderSpecifier[0])) .fetch(); } diff --git a/src/main/java/com/sparta/moit/domain/meeting/repository/RegionSecondRepository.java b/src/main/java/com/sparta/moit/domain/meeting/repository/RegionSecondRepository.java index d474453..5e14889 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/repository/RegionSecondRepository.java +++ b/src/main/java/com/sparta/moit/domain/meeting/repository/RegionSecondRepository.java @@ -8,7 +8,7 @@ import java.util.List; public interface RegionSecondRepository extends JpaRepository { - List findAllByRegionFirst(RegionFirst regionFirst); + List findAllByRegionFirstOrderByRegionSecondId(RegionFirst regionFirst); @Query(value = "SELECT s.*, f.region_first_id AS region_first_id_alias " + "FROM region_second s " diff --git a/src/main/java/com/sparta/moit/domain/meeting/service/MeetingService.java b/src/main/java/com/sparta/moit/domain/meeting/service/MeetingService.java index f098115..dbe664b 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/service/MeetingService.java +++ b/src/main/java/com/sparta/moit/domain/meeting/service/MeetingService.java @@ -1,10 +1,7 @@ package com.sparta.moit.domain.meeting.service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.sparta.moit.domain.meeting.dto.CreateMeetingRequestDto; -import com.sparta.moit.domain.meeting.dto.GetMeetingDetailResponseDto; -import com.sparta.moit.domain.meeting.dto.GetMeetingResponseDto; -import com.sparta.moit.domain.meeting.dto.UpdateMeetingRequestDto; +import com.sparta.moit.domain.meeting.dto.*; import com.sparta.moit.domain.member.entity.Member; import org.springframework.data.domain.Slice; @@ -14,11 +11,17 @@ public interface MeetingService { Long createMeeting(CreateMeetingRequestDto requestDto, Member member); + Long createMeetingArray(CreateMeetingRequestDto requestDto, Member member); Long updateMeeting(UpdateMeetingRequestDto requestDto, Member member, Long meetingId); + Long updateMeetingArray(UpdateMeetingRequestDto requestDto, Member member, Long meetingId); void deleteMeeting(Member member, Long meetingId); + Slice getMeetingListPostgreJson(int page ,Double locationLat ,Double locationLng ,String skillIdsStr ,String careerIdsStr); + + Slice getMeetingListPostgreArray(int page, Double locationLat, Double locationLng, String skillIdsStr, String careerIdsStr); + Slice getMeetingList(int page, Double locationLat, Double locationLng, List skillId, List careerId); List getMeetingListNativeQuery(int page, Double locationLat, Double locationLng, List skillId, List careerId); @@ -27,9 +30,11 @@ public interface MeetingService { List getMeetingListByAddress(String firstRegion, String secondRegion, int page) throws JsonProcessingException; - Slice getMeetingListBySearch(String keyword, int page); + Slice getMeetingListBySearch(String keyword, int page); Long enterMeeting(Member member, Long meetingId); void leaveMeeting(Member member, Long meetingId); + + List getPopularMeeting(); } diff --git a/src/main/java/com/sparta/moit/domain/meeting/service/MeetingServiceImpl.java b/src/main/java/com/sparta/moit/domain/meeting/service/MeetingServiceImpl.java index 205fef4..ccf0783 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/service/MeetingServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/meeting/service/MeetingServiceImpl.java @@ -1,10 +1,8 @@ package com.sparta.moit.domain.meeting.service; import com.fasterxml.jackson.core.JsonProcessingException; -import com.sparta.moit.domain.meeting.dto.CreateMeetingRequestDto; -import com.sparta.moit.domain.meeting.dto.GetMeetingDetailResponseDto; -import com.sparta.moit.domain.meeting.dto.GetMeetingResponseDto; -import com.sparta.moit.domain.meeting.dto.UpdateMeetingRequestDto; +import com.sparta.moit.domain.bookmark.repository.BookMarkRepository; +import com.sparta.moit.domain.meeting.dto.*; import com.sparta.moit.domain.meeting.entity.*; import com.sparta.moit.domain.meeting.repository.*; import com.sparta.moit.domain.member.entity.Member; @@ -13,11 +11,14 @@ import com.sparta.moit.global.error.CustomException; import com.sparta.moit.global.error.ErrorCode; import com.sparta.moit.global.util.AddressUtil; +import com.sparta.moit.global.util.pagination.ListPaginator; +import com.sparta.moit.global.util.pagination.Paginator; 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.SliceImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,20 +26,17 @@ import java.util.Objects; import java.util.Optional; -import static com.sparta.moit.domain.meeting.entity.QMeeting.meeting; - @Slf4j(topic = "Meeting Service Log") @Service @RequiredArgsConstructor public class MeetingServiceImpl implements MeetingService { private final MeetingRepository meetingRepository; - private final SkillRepository skillRepository; - private final CareerRepository careerRepository; private final MemberRepository memberRepository; private final MeetingMemberRepository meetingMemberRepository; - private final MeetingSkillRepository meetingSkillRepository; - private final MeetingCareerRepository meetingCareerRepository; private final AddressUtil addressUtil; + private final BookMarkRepository bookMarkRepository; + + private final Paginator paginator = new ListPaginator<>(); /*모임 등록*/ @Override @@ -47,8 +45,17 @@ public Long createMeeting(CreateMeetingRequestDto requestDto, Member member) { Meeting meeting = requestDto.toEntity(member); Meeting savedMeeting = meetingRepository.save(meeting); - saveSkills(requestDto.getSkillIds(), savedMeeting); - saveCareers(requestDto.getCareerIds(), savedMeeting); + saveMeetingMember(member, savedMeeting); + + return savedMeeting.getId(); + } + + @Override + @Transactional + public Long createMeetingArray(CreateMeetingRequestDto requestDto, Member member) { + Meeting meeting = requestDto.toEntityArray(member); + Meeting savedMeeting = meetingRepository.save(meeting); + saveMeetingMember(member, savedMeeting); return savedMeeting.getId(); @@ -62,14 +69,37 @@ public Long updateMeeting(UpdateMeetingRequestDto requestDto, Member member, Lon Meeting meeting = meetingRepository.findByIdAndCreator(meetingId, member) .orElseThrow(() -> new CustomException(ErrorCode.AUTHORITY_ACCESS)); - meetingSkillRepository.deleteByMeeting(meeting); - meetingCareerRepository.deleteByMeeting(meeting); + if (meeting.getStatus().equals(MeetingStatusEnum.DELETE)) { + throw new CustomException(ErrorCode.MEETING_DELETE); + } + + if (meeting.getStatus().equals(MeetingStatusEnum.COMPLETE)) { + throw new CustomException(ErrorCode.MEETING_COMPLETE); + } - saveSkills(requestDto.getSkillIds(), meeting); - saveCareers(requestDto.getCareerIds(), meeting); meeting.updateMeeting(requestDto); return meetingId; } + /*모임 수정*/ + @Override + @Transactional + public Long updateMeetingArray(UpdateMeetingRequestDto requestDto, Member member, Long meetingId) { + + Meeting meeting = meetingRepository.findByIdAndCreator(meetingId, member) + .orElseThrow(() -> new CustomException(ErrorCode.AUTHORITY_ACCESS)); + + if (meeting.getStatus().equals(MeetingStatusEnum.DELETE)) { + throw new CustomException(ErrorCode.MEETING_DELETE); + } + + if (meeting.getStatus().equals(MeetingStatusEnum.COMPLETE)) { + throw new CustomException(ErrorCode.MEETING_COMPLETE); + } + + meeting.updateMeetingArray(requestDto); + + return meetingId; + } /*모임 삭제*/ @Override @@ -83,6 +113,61 @@ public void deleteMeeting(Member member, Long meetingId) { } + @Override + public Slice getMeetingListPostgreJson( + int page + , Double locationLat + , Double locationLng + , String skillIdsStr + , String careerIdsStr + ) { + int extraItem = 1; // pagination 을 위한 추가 요청 + int pageSize = 10; + int offset = Math.max(page - 1, 0) * pageSize; + + List meetingList = meetingRepository.findMeetingST_Dwithin( + locationLng + , locationLat + , skillIdsStr + , careerIdsStr + , pageSize + extraItem + , offset + ); + + Pageable pageable = PageRequest.of(Math.max(page - 1, 0), pageSize); + boolean hasNext = hasNextPage(meetingList, pageable.getPageSize()); + List sliceList = meetingList.stream().limit(pageSize).map(GetMeetingResponseDto::fromEntity).toList(); + return new SliceImpl<>(sliceList, pageable, hasNext); + } + + @Override + public Slice getMeetingListPostgreArray( + int page + , Double locationLat + , Double locationLng + , String skillIdsStr + , String careerIdsStr + ) { + int extraItem = 1; // pagination 을 위한 추가 요청 + int pageSize = 10; + int offset = Math.max(page - 1, 0) * pageSize; + + List meetingList = meetingRepository.findMeetingST_Dwithin_array( + locationLng + , locationLat + , skillIdsStr + , careerIdsStr + , pageSize + extraItem + , offset + ); + + Pageable pageable = PageRequest.of(Math.max(page - 1, 0), pageSize); + boolean hasNext = hasNextPage(meetingList, pageable.getPageSize()); + List sliceList = meetingList.stream().limit(pageSize).map(GetMeetingArrayResponseDto::fromEntity).toList(); + return new SliceImpl<>(sliceList, pageable, hasNext); + } + + /*모임 조회*/ @Override public Slice getMeetingList(int page, Double locationLat, Double locationLng, List skillId, List careerId) { @@ -115,32 +200,25 @@ public List getMeetingListNativeQuery(int page, Double lo return meetingList.stream().map(GetMeetingResponseDto::fromEntity).toList(); } - /*모임 상세 조회 (비로그인)*/ -// @Override -// public GetMeetingDetailResponseDto getMeetingDetail(Long meetingId) { -// -// Meeting meeting = meetingRepository.findById(meetingId) -// .orElseThrow(() -> new CustomException(ErrorCode.MEETING_NOT_FOUND)); -// -// List careerNameList = meetingRepository.findCareerNameList(meetingId); -// List skillNameList = meetingRepository.findSkillNameList(meetingId); -// return GetMeetingDetailResponseDto.fromEntity(meeting, careerNameList, skillNameList, false); -// } - - /*모임 상세 조회 (로그인 유저) */ + + /*모임 상세 조회 */ @Override public GetMeetingDetailResponseDto getMeetingDetail(Long meetingId, Optional member) { Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(() -> new CustomException(ErrorCode.MEETING_NOT_FOUND)); - List careerNameList = meetingRepository.findCareerNameList(meetingId); - List skillNameList = meetingRepository.findSkillNameList(meetingId); + if (meeting.getStatus().equals(MeetingStatusEnum.DELETE)) { + throw new CustomException(ErrorCode.MEETING_NOT_FOUND); + } + if (member.isEmpty()) { - return GetMeetingDetailResponseDto.fromEntity(meeting, careerNameList, skillNameList, false); + return GetMeetingDetailResponseDto.fromEntity(meeting, false, false); } + boolean isJoin = meetingMemberRepository.existsByMemberIdAndMeetingId(member.get().getId(), meetingId); - return GetMeetingDetailResponseDto.fromEntity(meeting, careerNameList, skillNameList, isJoin); + boolean isbookMarked = bookMarkRepository.existsByMemberIdAndMeetingId(member.get().getId(), meetingId); + return GetMeetingDetailResponseDto.fromEntity(meeting, isJoin, isbookMarked); } /*주소별 모임 조회*/ @@ -153,10 +231,17 @@ public List getMeetingListByAddress(String firstRegion, S /* 모임 검색 */ @Override - public Slice getMeetingListBySearch(String keyword, int page) { + public Slice getMeetingListBySearch(String keyword, int page) { Pageable pageable = PageRequest.of(Math.max(page - 1, 0), 10); Slice meetingList = meetingRepository.findByKeyword(keyword, pageable); - return meetingList.map(GetMeetingResponseDto::fromEntity); + return meetingList.map(GetMeetingArrayResponseDto::fromEntity); + } + + /* 인기 모임 top 5 */ + @Override + public List getPopularMeeting() { + List meetingList = meetingRepository.getPopularMeetings(); + return meetingList.stream().map(GetPopularResponseDto::fromEntity).toList(); } /*모임 참가*/ @@ -223,36 +308,6 @@ public void leaveMeeting(Member member, Long meetingId) { meetingMemberRepository.delete(meetingMember); } - /*기술 저장*/ - private void saveSkills(List skillIds, Meeting meeting) { - for (Long skillId : skillIds) { - Skill skill = skillRepository.findById(skillId) - .orElseThrow(() -> new CustomException(ErrorCode.VALIDATION_ERROR)); - - MeetingSkill meetingSkill = MeetingSkill.builder() - .meeting(meeting) - .skill(skill) - .build(); - - meetingSkillRepository.save(meetingSkill); - } - } - - /*경력 저장*/ - private void saveCareers(List careerIds, Meeting meeting) { - for (Long careerId : careerIds) { - Career career = careerRepository.findById(careerId) - .orElseThrow(() -> new CustomException(ErrorCode.VALIDATION_ERROR)); - - MeetingCareer meetingCareer = MeetingCareer.builder() - .meeting(meeting) - .career(career) - .build(); - - meetingCareerRepository.save(meetingCareer); - } - } - /* 모임 회원 저장 */ private void saveMeetingMember(Member member, Meeting meeting) { MeetingMember meetingMember = MeetingMember.builder() @@ -261,4 +316,8 @@ private void saveMeetingMember(Member member, Meeting meeting) { .build(); meetingMemberRepository.save(meetingMember); } + + private boolean hasNextPage(List meetingList, int pageSize) { + return paginator.hasNextPage(meetingList, pageSize); + } } diff --git a/src/main/java/com/sparta/moit/domain/meeting/service/RegionService.java b/src/main/java/com/sparta/moit/domain/meeting/service/RegionService.java index 64ed7ca..2298134 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/service/RegionService.java +++ b/src/main/java/com/sparta/moit/domain/meeting/service/RegionService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.sparta.moit.domain.meeting.dto.RegionFirstResponseDto; +import com.sparta.moit.domain.meeting.dto.RegionIntegratedResponseDto; import com.sparta.moit.domain.meeting.dto.RegionSecondResponseDto; import com.sparta.moit.global.common.dto.AddressResponseDto; @@ -13,4 +14,6 @@ public interface RegionService { List getRegionSecond(Short regionFirstId); AddressResponseDto updateAddress() throws JsonProcessingException; + + List getRegion(); } diff --git a/src/main/java/com/sparta/moit/domain/meeting/service/RegionServiceImpl.java b/src/main/java/com/sparta/moit/domain/meeting/service/RegionServiceImpl.java index db303f6..f68e589 100644 --- a/src/main/java/com/sparta/moit/domain/meeting/service/RegionServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/meeting/service/RegionServiceImpl.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.sparta.moit.domain.meeting.dto.RegionFirstResponseDto; +import com.sparta.moit.domain.meeting.dto.RegionIntegratedResponseDto; import com.sparta.moit.domain.meeting.dto.RegionSecondResponseDto; import com.sparta.moit.domain.meeting.entity.RegionFirst; import com.sparta.moit.domain.meeting.entity.RegionSecond; @@ -26,6 +27,12 @@ public class RegionServiceImpl implements RegionService { private final RegionSecondRepository regionSecondRepository; private final AddressUtil addressUtil; + @Override + public List getRegion() { + List regionIntegegratedList = regionFirstRepository.findAll(); + return regionIntegegratedList.stream().map(RegionIntegratedResponseDto::fromEntity).toList(); + } + @Override public List getRegionFirst() { List regionFirstList = regionFirstRepository.findAll(); @@ -37,7 +44,7 @@ public List getRegionSecond(Short regionFirstId) { RegionFirst regionFirst = regionFirstRepository.findById(regionFirstId).orElseThrow(() -> new CustomException(ErrorCode.VALIDATION_ERROR) ); - List regionSecondList = regionSecondRepository.findAllByRegionFirst(regionFirst); + List regionSecondList = regionSecondRepository.findAllByRegionFirstOrderByRegionSecondId(regionFirst); return regionSecondList.stream().map(RegionSecondResponseDto::fromEntity).toList(); } diff --git a/src/main/java/com/sparta/moit/domain/member/controller/MemberController.java b/src/main/java/com/sparta/moit/domain/member/controller/MemberController.java index 371fa84..230a11a 100644 --- a/src/main/java/com/sparta/moit/domain/member/controller/MemberController.java +++ b/src/main/java/com/sparta/moit/domain/member/controller/MemberController.java @@ -1,55 +1,66 @@ package com.sparta.moit.domain.member.controller; import com.fasterxml.jackson.core.JsonProcessingException; +import com.sparta.moit.domain.member.controller.docs.MemberControllerDocs; import com.sparta.moit.domain.member.dto.MemberResponseDto; import com.sparta.moit.domain.member.service.KakaoService; +import com.sparta.moit.domain.member.service.MemberService; import com.sparta.moit.domain.member.service.NaverService; -import com.sparta.moit.global.common.dto.RefreshTokenRequest; import com.sparta.moit.global.common.dto.ResponseDto; +import com.sparta.moit.global.jwt.JwtUtil; +import com.sparta.moit.global.security.UserDetailsImpl; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/member") @RequiredArgsConstructor -public class MemberController { +public class MemberController implements MemberControllerDocs { private final KakaoService kakaoService; private final NaverService naverService; + private final MemberService memberService; + private final JwtUtil jwtUtil; /* 카카오 로그인 */ @GetMapping("/signin/kakao") - public ResponseEntity> kakaoLogin(@RequestParam String code) throws JsonProcessingException { + public ResponseEntity> kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException { MemberResponseDto responseDto = kakaoService.kakaoLogin(code); + /*accessToken, refreshToken cookie 추가*/ + jwtUtil.addAccessTokenToCookie(responseDto.getAccessToken(), response); + jwtUtil.addRefreshTokenToCookie(responseDto.getRefreshToken(), response); + return ResponseEntity.ok().body(ResponseDto.success("카카오 로그인 완료", responseDto)); + } /* 네이버 로그인 */ @GetMapping("/signin/naver") - public ResponseEntity> naverLogin(@RequestParam String code, @RequestParam String state) throws JsonProcessingException { + public ResponseEntity> naverLogin(@RequestParam String code, @RequestParam String state, HttpServletResponse response) throws JsonProcessingException { MemberResponseDto responseDto = naverService.naverLogin(code, state); + jwtUtil.addAccessTokenToCookie(responseDto.getAccessToken(), response); + jwtUtil.addRefreshTokenToCookie(responseDto.getRefreshToken(), response); return ResponseEntity.ok().body(ResponseDto.success("네이버 로그인 완료", responseDto)); } -// /*로그아웃 기능 호출*/ -// @PostMapping("/logout") -// public ResponseEntity logout(@RequestBody RefreshTokenRequest request) { -// /*리프레시 토큰이 없으면 badRequest 반환*/ -// String refreshTokenString = request.getRefreshToken(); -// if (refreshTokenString == null || refreshTokenString.isEmpty()) { -// return ResponseEntity.badRequest().build(); -// } -// -// /*로그아웃 API 호출*/ -// kakaoService.logout(refreshTokenString); -// /*로그아웃 메시지 반환*/ -// return ResponseEntity.status(HttpStatus.OK).body("로그아웃 되었습니다."); -// } + /* 회원탈퇴 */ + @DeleteMapping("/signout") + public ResponseEntity> signOut(@AuthenticationPrincipal UserDetailsImpl userDetails) { + memberService.signOut(userDetails.getUser()); + return ResponseEntity.ok().body(ResponseDto.success("회원 탈퇴 완료", "회원 탈퇴가 완료되었습니다.")); + } @GetMapping("/login") public ResponseEntity login() { String token = kakaoService.login(); return ResponseEntity.ok().body(token); } + + @GetMapping("/refresh-test") + public ResponseEntity refreshTest() { + String token = kakaoService.refreshTest(); + return ResponseEntity.ok().body(token); + } } \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/domain/member/controller/docs/MemberControllerDocs.java b/src/main/java/com/sparta/moit/domain/member/controller/docs/MemberControllerDocs.java index 0d5853c..0d377e6 100644 --- a/src/main/java/com/sparta/moit/domain/member/controller/docs/MemberControllerDocs.java +++ b/src/main/java/com/sparta/moit/domain/member/controller/docs/MemberControllerDocs.java @@ -1,21 +1,26 @@ package com.sparta.moit.domain.member.controller.docs; import com.fasterxml.jackson.core.JsonProcessingException; -import com.sparta.moit.global.common.dto.RefreshTokenRequest; +import com.sparta.moit.global.common.dto.ResponseDto; +import com.sparta.moit.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "로그인", description = "로그인 API") public interface MemberControllerDocs { @Operation(summary = "카카오 로그인", description = "카카오 로그인 API") - ResponseEntity kakaoLogin(@RequestParam String code) throws JsonProcessingException; + ResponseEntity kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException; @Operation(summary = "네이버 로그인", description = "네이버 로그인 API") - ResponseEntity naverLogin(@RequestParam String code,@RequestParam String state) throws JsonProcessingException; + ResponseEntity naverLogin(@RequestParam String code,@RequestParam String state, HttpServletResponse response) throws JsonProcessingException; + + @Operation(summary = "회원탈퇴", description = "회원 탈퇴 API") + ResponseEntity> signOut(@AuthenticationPrincipal UserDetailsImpl userDetails); + } diff --git a/src/main/java/com/sparta/moit/domain/member/entity/Member.java b/src/main/java/com/sparta/moit/domain/member/entity/Member.java index 5713930..2e14d7b 100644 --- a/src/main/java/com/sparta/moit/domain/member/entity/Member.java +++ b/src/main/java/com/sparta/moit/domain/member/entity/Member.java @@ -1,6 +1,5 @@ package com.sparta.moit.domain.member.entity; -import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import com.sparta.moit.domain.meeting.entity.MeetingMember; import jakarta.persistence.*; @@ -16,15 +15,26 @@ public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String username; + private String email; + private String password; + @Column(nullable = false) @Enumerated(value = EnumType.STRING) private UserRoleEnum role; + + // @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + private MemberStatusEnum status; + @Column(name = "kakao_id") private Long kakaoId; + @Column(name = "naver_id") private String naverId; @@ -32,20 +42,21 @@ public class Member { @JsonIgnore private List meetingMembers = new ArrayList<>(); - - public Member(String username, String password, String email, UserRoleEnum role) { + public Member(String username, String password, String email, UserRoleEnum role, MemberStatusEnum status) { this.username = username; this.password = password; this.email = email; this.role = role; + this.status = status; } @Builder - public Member (String username, String password, String email, UserRoleEnum role, Long kakaoId, String naverId){ + public Member(String username, String password, String email, UserRoleEnum role, MemberStatusEnum status, Long kakaoId, String naverId) { this.username = username; this.password = password; this.email = email; this.role = role; + this.status = status; this.kakaoId = kakaoId; this.naverId = naverId; } @@ -60,6 +71,10 @@ public Member updateNaverId(String naverId) { return this; } + public void signOutStatus() { + this.status = MemberStatusEnum.NONMEMBER; + } + /* 테스트용 */ public void setUsername(String username) { } diff --git a/src/main/java/com/sparta/moit/domain/member/entity/MemberStatusEnum.java b/src/main/java/com/sparta/moit/domain/member/entity/MemberStatusEnum.java new file mode 100644 index 0000000..ed3e0a2 --- /dev/null +++ b/src/main/java/com/sparta/moit/domain/member/entity/MemberStatusEnum.java @@ -0,0 +1,8 @@ +package com.sparta.moit.domain.member.entity; + +public enum MemberStatusEnum { + + NONMEMBER, /*비회원*/ + + MEMBER; /*회원*/ +} diff --git a/src/main/java/com/sparta/moit/domain/member/repository/MemberRepository.java b/src/main/java/com/sparta/moit/domain/member/repository/MemberRepository.java index 1447791..b8e4bc4 100644 --- a/src/main/java/com/sparta/moit/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/sparta/moit/domain/member/repository/MemberRepository.java @@ -2,12 +2,13 @@ import com.sparta.moit.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; +@Repository public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); - Optional findByKakaoId(Long kakaoId); Optional findByNaverId(String naverId); } diff --git a/src/main/java/com/sparta/moit/domain/member/service/KakaoService.java b/src/main/java/com/sparta/moit/domain/member/service/KakaoService.java index 12c2710..29fcbe8 100644 --- a/src/main/java/com/sparta/moit/domain/member/service/KakaoService.java +++ b/src/main/java/com/sparta/moit/domain/member/service/KakaoService.java @@ -2,10 +2,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.sparta.moit.domain.member.dto.MemberResponseDto; +import com.sparta.moit.domain.member.entity.Member; public interface KakaoService { MemberResponseDto kakaoLogin(String code) throws JsonProcessingException; // void logout(String refreshTokenString); String login(); + + String signOut(Member member) throws JsonProcessingException; + + String refreshTest(); } \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/domain/member/service/KakaoServiceImpl.java b/src/main/java/com/sparta/moit/domain/member/service/KakaoServiceImpl.java index fb21b02..0467b2b 100644 --- a/src/main/java/com/sparta/moit/domain/member/service/KakaoServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/member/service/KakaoServiceImpl.java @@ -6,10 +6,11 @@ import com.sparta.moit.domain.member.dto.KakaoUserInfoDto; import com.sparta.moit.domain.member.dto.MemberResponseDto; import com.sparta.moit.domain.member.entity.Member; +import com.sparta.moit.domain.member.entity.MemberStatusEnum; import com.sparta.moit.domain.member.entity.UserRoleEnum; import com.sparta.moit.domain.member.repository.MemberRepository; -import com.sparta.moit.global.jwt.JwtUtil; import com.sparta.moit.global.common.service.RefreshTokenService; +import com.sparta.moit.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -24,12 +25,13 @@ import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; +import java.time.LocalDateTime; import java.util.UUID; @Slf4j(topic = "Kakao Login") @Service @RequiredArgsConstructor -public class KakaoServiceImpl implements KakaoService{ +public class KakaoServiceImpl implements KakaoService { private final PasswordEncoder passwordEncoder; private final RestTemplate restTemplate; private final JwtUtil jwtUtil; @@ -45,9 +47,55 @@ public class KakaoServiceImpl implements KakaoService{ @Value("${kakao.redirect-uri}") private String redirectUri; + @Value("${kakao.admin-key}") + private String serviceAdminKey; + public String login() { return jwtUtil.createToken("brandy0108@daum.net", UserRoleEnum.USER); } + + public String refreshTest() { + return jwtUtil.createRefreshToken("brandy0108@daum.net", UserRoleEnum.USER); + } + + @Override + public String signOut(Member member) throws JsonProcessingException { + /* 요청 URL 만들기 */ + URI uri = UriComponentsBuilder + .fromUriString("https://kauth.kakao.com") + .path("/user/unlink") + .encode() + .build() + .toUri(); + + /* HTTP Header 생성 */ + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + headers.add("Authorization", "KakaoAK " + serviceAdminKey); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("target_id_type", "user_id"); + body.add("target_id", String.valueOf(member.getKakaoId())); + + RequestEntity> requestEntity = RequestEntity + .post(uri) + .headers(headers) + .body(body); + + /* HTTP 요청 보내기 */ + ResponseEntity response = restTemplate.exchange( + requestEntity, + String.class + ); + + /* HTTP 응답 (JSON) -> 카카오ID 파싱 */ + JsonNode jsonNode = new ObjectMapper().readTree(response.getBody()); + String kakaoId = jsonNode.get("id").asText(); + + /* access token 반환 */ + return kakaoId; + } + public MemberResponseDto kakaoLogin(String code) throws JsonProcessingException { /* 1. "인가 코드"로 "액세스 토큰" 요청 */ String accessToken = getToken(code); @@ -57,6 +105,11 @@ public MemberResponseDto kakaoLogin(String code) throws JsonProcessingException /* 3. 필요시에 회원가입 */ Member kakaoMember = registerKakaoUserIfNeeded(kakaoUserInfo); + if (kakaoMember == null) { + log.error("Kakao 사용자 등록 실패"); + throw new RuntimeException("Kakao 사용자 등록 실패"); + } + log.info("Kakao 사용자 등록 완료: " + kakaoMember); /* 4. JWT 토큰 반환 */ /* 이게 우리서버 Access Token */ @@ -73,7 +126,6 @@ public MemberResponseDto kakaoLogin(String code) throws JsonProcessingException } private String getToken(String code) throws JsonProcessingException { - log.info("인가코드 : " + code); /* 요청 URL 만들기 */ URI uri = UriComponentsBuilder .fromUriString("https://kauth.kakao.com") @@ -114,7 +166,6 @@ private String getToken(String code) throws JsonProcessingException { } private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException { - log.info("accessToken : " + accessToken); /* 요청 URL 만들기 */ URI uri = UriComponentsBuilder @@ -148,6 +199,7 @@ private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcess String email = jsonNode.get("kakao_account") .get("email").asText(); + log.info("서버 로그인 시간: " + LocalDateTime.now()); log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email); return new KakaoUserInfoDto(id, nickname, email); } @@ -179,6 +231,7 @@ private Member registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) { .password(encodedPassword) .email(kakaoUserInfo.getEmail()) .role(UserRoleEnum.USER) + .status(MemberStatusEnum.MEMBER) .kakaoId(kakaoId) .build(); } @@ -187,9 +240,4 @@ private Member registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) { } return kakaoUser; } - -// /* 로그아웃 */ -// public void deleteRefreshToken(String refreshTokenString){ -// refreshTokenService.deleteRefreshToken(refreshTokenString); -// } } \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/domain/member/service/MemberService.java b/src/main/java/com/sparta/moit/domain/member/service/MemberService.java index 2ad6431..5b6bc10 100644 --- a/src/main/java/com/sparta/moit/domain/member/service/MemberService.java +++ b/src/main/java/com/sparta/moit/domain/member/service/MemberService.java @@ -1,4 +1,9 @@ package com.sparta.moit.domain.member.service; +import com.sparta.moit.domain.member.entity.Member; + public interface MemberService { + + void signOut(Member member); + } diff --git a/src/main/java/com/sparta/moit/domain/member/service/MemberServiceImpl.java b/src/main/java/com/sparta/moit/domain/member/service/MemberServiceImpl.java index e751764..acff597 100644 --- a/src/main/java/com/sparta/moit/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/member/service/MemberServiceImpl.java @@ -1,8 +1,37 @@ package com.sparta.moit.domain.member.service; +import com.sparta.moit.domain.member.entity.Member; +import com.sparta.moit.domain.member.repository.MemberRepository; +import com.sparta.moit.global.error.CustomException; +import com.sparta.moit.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class MemberServiceImpl implements MemberService { + private final MemberRepository memberRepository; + private final KakaoService kakaoService; + + /*회원 탈퇴*/ + @Override + @Transactional + public void signOut(Member member) { + + Member member1 = memberRepository.findById(member.getId()) + .orElseThrow(() -> new IllegalArgumentException("해당 회원이 없습니다. memberId= " + member)); + + System.out.println(member1.getKakaoId()); + /*카카오 unlink*/ + if (member1.getKakaoId() != null) { + try { + kakaoService.signOut(member1); + } catch (Exception e) { + throw new CustomException(ErrorCode.KAKAOID_UNLINK_FAILURE); + } + } + member1.signOutStatus(); + } } diff --git a/src/main/java/com/sparta/moit/domain/member/service/NaverServiceImpl.java b/src/main/java/com/sparta/moit/domain/member/service/NaverServiceImpl.java index 277897f..c1dd300 100644 --- a/src/main/java/com/sparta/moit/domain/member/service/NaverServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/member/service/NaverServiceImpl.java @@ -6,11 +6,12 @@ import com.sparta.moit.domain.member.dto.MemberResponseDto; import com.sparta.moit.domain.member.dto.NaverUserInfoDto; import com.sparta.moit.domain.member.entity.Member; +import com.sparta.moit.domain.member.entity.MemberStatusEnum; import com.sparta.moit.domain.member.entity.UserRoleEnum; import com.sparta.moit.domain.member.repository.MemberRepository; +import com.sparta.moit.global.common.service.RefreshTokenService; import com.sparta.moit.global.error.CustomValidationException; import com.sparta.moit.global.jwt.JwtUtil; -import com.sparta.moit.global.common.service.RefreshTokenService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -31,7 +32,7 @@ @Slf4j(topic = "Naver Login") @Service @RequiredArgsConstructor -public class NaverServiceImpl implements NaverService{ +public class NaverServiceImpl implements NaverService { private final PasswordEncoder passwordEncoder; private final RestTemplate restTemplate; private final JwtUtil jwtUtil; @@ -48,7 +49,7 @@ public class NaverServiceImpl implements NaverService{ private String redirectUri; public MemberResponseDto naverLogin(String code, String state) throws JsonProcessingException { - String accessToken = getAccessToken(code,state); + String accessToken = getAccessToken(code, state); NaverUserInfoDto naverUserInfo = getNaverUserInfo(accessToken); Member naverMember = registerNaverUserIfNeeded(naverUserInfo); String createToken = jwtUtil.createToken(naverMember.getEmail(), naverMember.getRole()); @@ -71,7 +72,7 @@ private String getAccessToken(String code, String state) throws JsonProcessingEx .queryParam("client_secret", clientSecret) .queryParam("redirect_uri", redirectUri) .queryParam("code", code) - .queryParam("state",state) + .queryParam("state", state) .encode() .build() .toUri(); @@ -156,6 +157,7 @@ private Member registerNaverUserIfNeeded(NaverUserInfoDto naverUserInfo) { .password(encodedPassword) .email(naverUserInfo.getEmail()) .role(UserRoleEnum.USER) + .status(MemberStatusEnum.MEMBER) .naverId(naverId) .build(); } @@ -164,17 +166,13 @@ private Member registerNaverUserIfNeeded(NaverUserInfoDto naverUserInfo) { } return naverUser; } + /* refreshAccessToken */ private String refreshAccessToken(String refreshToken) { Optional newAccessToken = refreshTokenService.refreshAccessToken(refreshToken); return newAccessToken.orElseThrow(() -> new CustomValidationException("Failed to refresh access token", null)); } -// /* 로그아웃 */ -// public void logout(String refreshTokenString){ -// refreshTokenService.deleteRefreshToken(refreshTokenString); -// } - public String refreshToken(String refreshToken) { return refreshAccessToken(refreshToken); } diff --git a/src/main/java/com/sparta/moit/domain/mypage/controller/MypageController.java b/src/main/java/com/sparta/moit/domain/mypage/controller/MypageController.java index 72ccfb0..21d4955 100644 --- a/src/main/java/com/sparta/moit/domain/mypage/controller/MypageController.java +++ b/src/main/java/com/sparta/moit/domain/mypage/controller/MypageController.java @@ -36,4 +36,24 @@ public ResponseEntity>> getMypageMeet List responseDtoList = mypageService.getMypageMeetingList(userDetails.getUser().getId()); return ResponseEntity.ok().body(ResponseDto.success("마이페이지 조회 완료", responseDtoList)); } + + /* 개최한 모임 정보 리스트 */ + @GetMapping("/meeting/held") + public ResponseEntity>> getMypageHeldList(@AuthenticationPrincipal UserDetailsImpl userDetails){ + List responseDtoList = mypageService.getMypageHeldList(userDetails.getUser().getId()); + return ResponseEntity.ok().body(ResponseDto.success("개최한 모임 조회 완료", responseDtoList)); + } + + /* 완료한 모임 정보 리스트 */ + @GetMapping("/meeting/complete") + public ResponseEntity>> getCompletedMeetings(@AuthenticationPrincipal UserDetailsImpl userDetails) { + List responseDtoList = mypageService.getCompletedMeetings(userDetails.getUser().getId()); + return ResponseEntity.ok().body(ResponseDto.success("완료된 모임 조회 완료", responseDtoList)); + } + /* 북마크 된 모임 리스트 */ + @GetMapping("/meeting/bookmarked") + public ResponseEntity>> getMypageBookmarkedMeetings(@AuthenticationPrincipal UserDetailsImpl userDetails) { + List responseDtoList = mypageService.getMypageBookmarkedMeetings(userDetails.getUser().getId()); + return ResponseEntity.ok().body(ResponseDto.success("북마크된 모임 조회 완료", responseDtoList)); + } } diff --git a/src/main/java/com/sparta/moit/domain/mypage/controller/docs/MypageControllerDocs.java b/src/main/java/com/sparta/moit/domain/mypage/controller/docs/MypageControllerDocs.java index 6409742..420a46a 100644 --- a/src/main/java/com/sparta/moit/domain/mypage/controller/docs/MypageControllerDocs.java +++ b/src/main/java/com/sparta/moit/domain/mypage/controller/docs/MypageControllerDocs.java @@ -1,16 +1,25 @@ package com.sparta.moit.domain.mypage.controller.docs; +import com.sparta.moit.domain.mypage.dto.MypageMeetingResponseDto; +import com.sparta.moit.global.common.dto.ResponseDto; import com.sparta.moit.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; public interface MypageControllerDocs { @Operation(summary = "마이페이지 정보 조회", description = "마이페이지 정보 조회 API") ResponseEntity getMypage(@AuthenticationPrincipal UserDetailsImpl userDetails); - @Operation(summary = "마이페이지 참여 모임 리스트", description = "마이페이지 참여 모임 리스트 API") ResponseEntity getMypageMeetingList(@AuthenticationPrincipal UserDetailsImpl userDetails); - + @Operation(summary = "마이페이지 개최 모임 리스트", description = "마이페이지 개최한 모임 리스트 API") + public ResponseEntity>> getMypageHeldList(@AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "마이페이지 참여 완료 리스트", description = "마이페이지 참여 완료 리스트 API") + public ResponseEntity>> getCompletedMeetings(@AuthenticationPrincipal UserDetailsImpl userDetails); + @Operation(summary = "북마크 정보 조회", description = "북마크 정보 리스트 API") + ResponseEntity>> getMypageBookmarkedMeetings(@AuthenticationPrincipal UserDetailsImpl userDetails); } diff --git a/src/main/java/com/sparta/moit/domain/mypage/dto/MypageMeetingResponseDto.java b/src/main/java/com/sparta/moit/domain/mypage/dto/MypageMeetingResponseDto.java index 8ebbf91..50dda21 100644 --- a/src/main/java/com/sparta/moit/domain/mypage/dto/MypageMeetingResponseDto.java +++ b/src/main/java/com/sparta/moit/domain/mypage/dto/MypageMeetingResponseDto.java @@ -5,37 +5,60 @@ import lombok.Builder; import lombok.Getter; +import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; @Getter public class MypageMeetingResponseDto { private final Long meetingId; + private final String meetingName; - @JsonFormat(pattern = "yyyy-MM-dd") - private final ZonedDateTime meetingDate; + + private final LocalDate meetingDate; + @JsonFormat(pattern = "HH:mm") - private final ZonedDateTime meetingStartTime; + private final LocalDateTime meetingStartTime; + @JsonFormat(pattern = "HH:mm") - private final ZonedDateTime meetingEndTime; + private final LocalDateTime meetingEndTime; + + private final String status; + + private boolean isBookmarked; @Builder - public MypageMeetingResponseDto(Long meetingId, String meetingName, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime) { + public MypageMeetingResponseDto(Long meetingId, String meetingName, LocalDate meetingDate, LocalDateTime meetingStartTime, LocalDateTime meetingEndTime, String status, boolean isBookmarked) { this.meetingId = meetingId; this.meetingName = meetingName; - this.meetingDate = meetingStartTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.meetingStartTime = meetingStartTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); - this.meetingEndTime = meetingEndTime.atZone(ZoneId.of("UTC")).withZoneSameInstant(ZoneId.of("Asia/Seoul")); + this.meetingDate = meetingDate; + this.meetingStartTime = meetingStartTime; + this.meetingEndTime = meetingEndTime; + this.status = status; + this.isBookmarked = isBookmarked; + } + public static MypageMeetingResponseDto fromEntity(Meeting meeting, boolean isBookmarked){ + String status = meeting.getStatus().toString(); /* Enum을 문자열로 변환 */ + return MypageMeetingResponseDto.builder() + .meetingId(meeting.getId()) + .meetingName(meeting.getMeetingName()) + .meetingDate(meeting.getMeetingDate()) + .meetingStartTime(meeting.getMeetingStartTime()) + .meetingEndTime(meeting.getMeetingEndTime()) + .status(status) + .isBookmarked(isBookmarked) + .build(); } public static MypageMeetingResponseDto fromEntity(Meeting meeting){ + String status = meeting.getStatus().toString(); /* Enum을 문자열로 변환 */ return MypageMeetingResponseDto.builder() .meetingId(meeting.getId()) .meetingName(meeting.getMeetingName()) + .meetingDate(meeting.getMeetingDate()) .meetingStartTime(meeting.getMeetingStartTime()) .meetingEndTime(meeting.getMeetingEndTime()) + .status(status) .build(); } } diff --git a/src/main/java/com/sparta/moit/domain/mypage/service/MypageService.java b/src/main/java/com/sparta/moit/domain/mypage/service/MypageService.java index 8d89301..f089935 100644 --- a/src/main/java/com/sparta/moit/domain/mypage/service/MypageService.java +++ b/src/main/java/com/sparta/moit/domain/mypage/service/MypageService.java @@ -12,4 +12,7 @@ public interface MypageService { MypageResponseDto getMypageInfo(Member member); long calculateStudyTime(LocalDateTime meetingStartTime, LocalDateTime meetingEndTime); List getMypageMeetingList(Long memberId); + List getMypageHeldList(Long memberId); + List getCompletedMeetings(Long memberId); + List getMypageBookmarkedMeetings(Long memberId); } diff --git a/src/main/java/com/sparta/moit/domain/mypage/service/MypageServiceImpl.java b/src/main/java/com/sparta/moit/domain/mypage/service/MypageServiceImpl.java index 2a0730e..4ff077d 100644 --- a/src/main/java/com/sparta/moit/domain/mypage/service/MypageServiceImpl.java +++ b/src/main/java/com/sparta/moit/domain/mypage/service/MypageServiceImpl.java @@ -1,5 +1,6 @@ package com.sparta.moit.domain.mypage.service; +import com.sparta.moit.domain.bookmark.repository.BookMarkRepository; import com.sparta.moit.domain.meeting.dto.GetMyPageDto; import com.sparta.moit.domain.meeting.entity.Meeting; import com.sparta.moit.domain.meeting.entity.MeetingStatusEnum; @@ -18,6 +19,8 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.List; @Slf4j(topic = "Mypage") @@ -27,6 +30,7 @@ public class MypageServiceImpl implements MypageService { private final MemberRepository memberRepository; private final MeetingMemberRepository meetingMemberRepository; private final MeetingRepository meetingRepository; + private final BookMarkRepository bookMarkRepository; /*select * from meeting @@ -51,9 +55,8 @@ public MypageResponseDto getMypageInfo(Member member) { int heldMeetingCount = meetingRepository.countByCreatorAndStatusNot(member, MeetingStatusEnum.DELETE); // TODO: meeting 중 status != DELETE 인 것만 count 하도록 변경 - - List studyTimeList = meetingRepository.getMyPage(member.getId(), MeetingStatusEnum.DELETE); - // TODO: meeting 중 status != DELETE 인 것만 count 하도록 변경 + List studyTimeList = meetingRepository.getMyPage(member.getId(), MeetingStatusEnum.COMPLETE); + // TODO: meeting 중 status = COMPLETE 인 것만 count 하도록 변경 /* 총 공부시간 */ long totalStudyTimeMinutes = 0; @@ -78,7 +81,18 @@ public MypageResponseDto getMypageInfo(Member member) { public long calculateStudyTime(LocalDateTime meetingStartTime, LocalDateTime meetingEndTime) { if (meetingStartTime != null && meetingEndTime != null) { + /* meetingStartTime이 meetingEndTime 이후나 같은지 체크*/ + if (meetingStartTime.isAfter(meetingEndTime) || meetingStartTime.equals(meetingEndTime)) { + throw new IllegalArgumentException("meetingStartTime은 meetingEndTime보다 이전이어야 합니다."); + } + Duration duration = Duration.between(meetingStartTime, meetingEndTime); + + /* - 시간 방지 */ + if (duration.isNegative()) { + throw new IllegalArgumentException("meetingStartTime은 meetingEndTime보다 이전이어야 합니다."); + } + return duration.toMinutes(); } else { return 0; @@ -88,7 +102,38 @@ public long calculateStudyTime(LocalDateTime meetingStartTime, LocalDateTime mee @Override public List getMypageMeetingList(Long memberId) { List meetingList = meetingRepository.findMeetingsByMember(memberId); - return meetingList.stream().map(MypageMeetingResponseDto::fromEntity).toList(); + return meetingList.stream() + .filter(meeting -> meeting.getStatus() == MeetingStatusEnum.OPEN || + meeting.getStatus() == MeetingStatusEnum.FULL || + meeting.getStatus() == MeetingStatusEnum.COMPLETE) + .map(MypageMeetingResponseDto::fromEntity) + .toList(); + } + + @Override + public List getMypageHeldList(Long memberId) { + List heldMeetingList = meetingRepository.findMeetingsByCreatorIdAndStatusNot(memberId, MeetingStatusEnum.DELETE); + return heldMeetingList.stream() + .filter(meeting -> meeting.getStatus() == MeetingStatusEnum.OPEN || + meeting.getStatus() == MeetingStatusEnum.FULL || + meeting.getStatus() == MeetingStatusEnum.COMPLETE) + .map(MypageMeetingResponseDto::fromEntity) + .toList(); + } + + @Override + public List getCompletedMeetings(Long memberId) { + List completedMeetingList = meetingRepository.findMeetingsByCreatorIdAndStatus(memberId, MeetingStatusEnum.COMPLETE); + return completedMeetingList.stream() + .map(MypageMeetingResponseDto::fromEntity) + .toList(); + } + + @Override + public List getMypageBookmarkedMeetings(Long memberId) { + List bookmarkedMeetings = bookMarkRepository.findBookmarkedMeetingsByMemberId(memberId); + return bookmarkedMeetings.stream() + .map(meeting -> MypageMeetingResponseDto.fromEntity(meeting, true)) + .toList(); } - // TODO : OPEN, FULL / COMPLETE 인것 분리해서 api 작성, 모두 무한 스크롤로 구성 (pageSize 10개) } \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/global/aspect/PerformanceAspect.java b/src/main/java/com/sparta/moit/global/aspect/PerformanceAspect.java new file mode 100644 index 0000000..bd4b2b1 --- /dev/null +++ b/src/main/java/com/sparta/moit/global/aspect/PerformanceAspect.java @@ -0,0 +1,28 @@ +package com.sparta.moit.global.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Slf4j +public class PerformanceAspect { + @Around("execution(* com.sparta.moit.domain..controller.*.*(..))") + public Object measureClassMethodExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + long startTime = System.currentTimeMillis(); + Object returnValue = joinPoint.proceed(); + long totalTime = System.currentTimeMillis() - startTime; + + String methodName = joinPoint.getSignature().getName(); + String className = joinPoint.getTarget().getClass().getSimpleName(); + if (totalTime > 1000L) { + log.warn("[" + className + "." + methodName + "] took " + totalTime + " ms."); + return returnValue; + } + log.info("[" + className + "." + methodName + "] took " + totalTime + " ms."); + return returnValue; + } +} diff --git a/src/main/java/com/sparta/moit/global/common/controller/AuthController.java b/src/main/java/com/sparta/moit/global/common/controller/AuthController.java index 4c2b32c..5ab6502 100644 --- a/src/main/java/com/sparta/moit/global/common/controller/AuthController.java +++ b/src/main/java/com/sparta/moit/global/common/controller/AuthController.java @@ -1,62 +1,87 @@ package com.sparta.moit.global.common.controller; -import com.sparta.moit.domain.member.service.KakaoService; -import com.sparta.moit.domain.member.service.KakaoServiceImpl; import com.sparta.moit.global.common.controller.docs.AuthControllerDocs; import com.sparta.moit.global.common.dto.RefreshTokenRequest; import com.sparta.moit.global.common.dto.ResponseDto; +import com.sparta.moit.global.common.service.RedisService; import com.sparta.moit.global.common.service.RefreshTokenService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpHeaders; +import com.sparta.moit.global.jwt.JwtUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import java.util.Optional; @RestController @RequestMapping("/api/auth") +@RequiredArgsConstructor +@Slf4j(topic = "authcontroller") public class AuthController implements AuthControllerDocs { private final RefreshTokenService refreshTokenService; - private final KakaoService kakaoService; + private final JwtUtil jwtUtil; + private final RedisService redisService; - public AuthController(RefreshTokenService refreshTokenService, KakaoService kakaoService) { - this.refreshTokenService = refreshTokenService; - this.kakaoService = kakaoService; - } - - /*엑세스 토큰 갱신 API 호출*/ - @PostMapping("/refresh") - @ApiResponse(responseCode = "200", description = "엑세스 토큰 발급 성공") - public ResponseEntity refreshAccessToken(@RequestBody RefreshTokenRequest request) { + @GetMapping("/refresh") + public ResponseEntity refreshAccessToken(HttpServletRequest request, HttpServletResponse response) { /*리프레시 토큰이 없으면 badRequest 반환*/ - String refreshToken = request.getRefreshToken(); + String refreshToken = jwtUtil.getRefreshTokenFromCookie(request); if (refreshToken == null || refreshToken.isEmpty()) { return ResponseEntity.badRequest().body("토큰이 입력되지 않았습니다."); } /*리프레시 토큰 검증*/ - boolean isValidRefreshToken = refreshTokenService.validateRefreshToken(refreshToken); + boolean isValidRefreshToken = redisService.validateRefreshToken(refreshToken); if (!isValidRefreshToken) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증이 필요합니다."); } - /*리프레시 토큰으로 새로운 엑세스 토큰 발급*/ - return refreshTokenService.refreshAccessToken(refreshToken) - .map(accessToken -> { - HttpHeaders headers = new HttpHeaders(); - headers.add(AUTHORIZATION, accessToken); - return ResponseEntity.ok().body(ResponseDto.success("액세스 토큰이 발급되었습니다.", accessToken)); - }) - .orElseGet(() -> ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.fail("엑세스 토큰 발급에 실패했습니다.", null))); + Optional newAccessToken = refreshTokenService.refreshAccessToken(refreshToken); + + /*accessToken, refreshToken cookie 추가*/ + if (newAccessToken.isEmpty()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.fail("엑세스 토큰 발급에 실패했습니다.", null)); + } else { + jwtUtil.addAccessTokenToCookie(String.valueOf(newAccessToken), response); + jwtUtil.addRefreshTokenToCookie(refreshToken, response); + } + return ResponseEntity.ok().body(ResponseDto.success("액세스 토큰이 발급되었습니다.", newAccessToken)); } + +// /*엑세스 토큰 갱신 API 호출*/ +// @PostMapping("/refresh") +// @ApiResponse(responseCode = "200", description = "엑세스 토큰 발급 성공") +// public ResponseEntity refreshAccessToken(@RequestBody RefreshTokenRequest request) { +// /*리프레시 토큰이 없으면 badRequest 반환*/ +// String refreshToken = request.getRefreshToken(); +// +// if (refreshToken == null || refreshToken.isEmpty()) { +// return ResponseEntity.badRequest().body("토큰이 입력되지 않았습니다."); +// } +// +// /*리프레시 토큰 검증*/ +// boolean isValidRefreshToken = refreshTokenService.validateRefreshToken(refreshToken); +// +// if (!isValidRefreshToken) { +// return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증이 필요합니다."); +// } +// +// /*리프레시 토큰으로 새로운 엑세스 토큰 발급*/ +// return refreshTokenService.refreshAccessToken(refreshToken) +// .map(accessToken -> { +// HttpHeaders headers = new HttpHeaders(); +// headers.add(AUTHORIZATION, accessToken); +// return ResponseEntity.ok().body(ResponseDto.success("액세스 토큰이 발급되었습니다.", accessToken)); +// }) +// .orElseGet(() -> ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDto.fail("엑세스 토큰 발급에 실패했습니다.", null))); +// } + + /*로그아웃 기능 호출*/ @PostMapping("/logout") public ResponseEntity deleteRefreshToken(@RequestBody RefreshTokenRequest request) { diff --git a/src/main/java/com/sparta/moit/global/common/controller/CommonController.java b/src/main/java/com/sparta/moit/global/common/controller/CommonController.java index 8263ccc..587237c 100644 --- a/src/main/java/com/sparta/moit/global/common/controller/CommonController.java +++ b/src/main/java/com/sparta/moit/global/common/controller/CommonController.java @@ -1,34 +1,19 @@ package com.sparta.moit.global.common.controller; -import com.sparta.moit.domain.member.dto.LoginRequestDto; -import com.sparta.moit.domain.member.dto.MemberResponseDto; -import com.sparta.moit.domain.member.service.MemberService; -import com.sparta.moit.global.common.dto.RefreshTokenResponse; -//import com.sparta.moit.global.service.TestService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; @Tag(name = "공통", description = "공통 API") @Slf4j(topic = "CommonController") @RestController public class CommonController { -// private final TestService testService; -// -// @Autowired -// public CommonController(TestService testService) { -// this.testService = testService; -// } - @Operation(summary = "연결 체크", description = "연결 체크 API") @GetMapping("/") public String healthCheck1() { - log.info("Connection OK"); return "Connection OK"; } @@ -39,30 +24,4 @@ public String healthCheck2() { return "Connection OK"; } - /*@PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequestDto loginRequestDto) { - try { - // 사용자의 토큰 생성 - String token = testService.generateUserToken(loginRequestDto.getEmail(), loginRequestDto.getPassword()); - - if (token != null && !token.isEmpty()) { - // 생성된 토큰을 응답 헤더에 추가 - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.set("Authorization", token); - - return ResponseEntity.ok().headers(responseHeaders).body("Login successful"); - } else { - return ResponseEntity.badRequest().body("Failed to generate token"); - } - } catch (Exception e) { - log.error("로그인 실패: {}", e.getMessage()); - return ResponseEntity.badRequest().body("로그인 실패"); - } - }*/ -// @GetMapping("/login") -// public ResponseEntity login(){ -// MemberResponseDto responseDto = testService.login(); -// return ResponseEntity.ok().body(responseDto); -// } - } \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/global/common/controller/docs/AuthControllerDocs.java b/src/main/java/com/sparta/moit/global/common/controller/docs/AuthControllerDocs.java index b9f84e5..599e194 100644 --- a/src/main/java/com/sparta/moit/global/common/controller/docs/AuthControllerDocs.java +++ b/src/main/java/com/sparta/moit/global/common/controller/docs/AuthControllerDocs.java @@ -3,13 +3,15 @@ import com.sparta.moit.global.common.dto.RefreshTokenRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; -@Tag(name = "Auth Controller", description = "리프레시 토큰을 통한 엑세스 토큰 갱신") +@Tag(name = "AuthController", description = "리프레시 토큰을 통한 엑세스 토큰 갱신") public interface AuthControllerDocs { @Operation(summary = "엑세스 토큰 갱신", description = "리프레시 토큰 검증 및 새 엑세스 토큰 발급") - ResponseEntity refreshAccessToken(@RequestBody RefreshTokenRequest request); + ResponseEntity refreshAccessToken(HttpServletRequest request, HttpServletResponse response); @Operation(summary = "로그아웃", description = "로그아웃 시 RefreshToken을 만료처리 합니다.") ResponseEntity deleteRefreshToken(@RequestBody RefreshTokenRequest request); diff --git a/src/main/java/com/sparta/moit/global/common/service/RedisService.java b/src/main/java/com/sparta/moit/global/common/service/RedisService.java index ff8c4d9..7779cb0 100644 --- a/src/main/java/com/sparta/moit/global/common/service/RedisService.java +++ b/src/main/java/com/sparta/moit/global/common/service/RedisService.java @@ -15,7 +15,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -@Slf4j +@Slf4j(topic = "redisService ") @Component @RequiredArgsConstructor public class RedisService { @@ -29,6 +29,11 @@ public void saveRefreshToken(RedisRefreshToken refreshToken) { public Optional findRefreshToken(String token) { return redisRefreshTokenRepository.findById(token); } + + public boolean validateRefreshToken(String token) { + Optional redisRefreshToken = redisRefreshTokenRepository.findById(token); + return redisRefreshTokenRepository.findById(token).isPresent(); + } public void setValues(String key, String data) { ValueOperations values = redisTemplate.opsForValue(); values.set(key, data); diff --git a/src/main/java/com/sparta/moit/global/common/service/RefreshTokenService.java b/src/main/java/com/sparta/moit/global/common/service/RefreshTokenService.java index be8435d..013a405 100644 --- a/src/main/java/com/sparta/moit/global/common/service/RefreshTokenService.java +++ b/src/main/java/com/sparta/moit/global/common/service/RefreshTokenService.java @@ -40,6 +40,8 @@ public String createAndSaveRefreshToken(String email, String refreshTokenString) /*리프레시 토큰 검증*/ public boolean validateRefreshToken(String token) { + log.info("validateRefreshToken 검증하는 리프레시 토큰: "+ token); + return redisRefreshTokenRepository.existsByToken(token); } diff --git a/src/main/java/com/sparta/moit/global/config/RedisConfig.java b/src/main/java/com/sparta/moit/global/config/RedisConfig.java index 4155a52..22f3c28 100644 --- a/src/main/java/com/sparta/moit/global/config/RedisConfig.java +++ b/src/main/java/com/sparta/moit/global/config/RedisConfig.java @@ -29,6 +29,7 @@ public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } /* serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 diff --git a/src/main/java/com/sparta/moit/global/config/WebSecurityConfig.java b/src/main/java/com/sparta/moit/global/config/WebSecurityConfig.java index 7a760d5..3df0415 100644 --- a/src/main/java/com/sparta/moit/global/config/WebSecurityConfig.java +++ b/src/main/java/com/sparta/moit/global/config/WebSecurityConfig.java @@ -100,6 +100,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/member/signin/kakao").permitAll() .requestMatchers("/api/member/signin/naver").permitAll() .requestMatchers("/api/member/login").permitAll() + .requestMatchers("/api/member/refresh-test").permitAll() .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/ws/**").permitAll() .requestMatchers("/app/api/meetings/**").permitAll() diff --git a/src/main/java/com/sparta/moit/global/error/ErrorCode.java b/src/main/java/com/sparta/moit/global/error/ErrorCode.java index 942d0a1..9d461e2 100644 --- a/src/main/java/com/sparta/moit/global/error/ErrorCode.java +++ b/src/main/java/com/sparta/moit/global/error/ErrorCode.java @@ -39,6 +39,8 @@ public enum ErrorCode { DUPLICATED_MEMBER("DUPLICATED_MEMBER", "중복된 회원입니다.", HttpStatus.BAD_REQUEST), ALREADY_MEMBER("ALREADY_MEMBER", "이미 참가한 회원입니다.", HttpStatus.BAD_REQUEST), MEETING_FULL("MEETING_FULL", "가득찬 모임입니다.", HttpStatus.BAD_REQUEST), + MEETING_COMPLETE("MEETING_COMPLETE", "완료된 모임입니다.", HttpStatus.BAD_REQUEST), + MEETING_DELETE("MEETING_DELETE", "삭제된 모임입니다.", HttpStatus.BAD_REQUEST), NOT_EXIST_USER("NOT_EXIST_USER", "해당 유저는 존재하지 않습니다.", HttpStatus.BAD_REQUEST), NOT_MATCH_PWD("NOT_MATCH_PWD", "비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST), AUTHORITY_ACCESS("AUTHORITY_ACCESS", "접근 권한이 없습니다.", HttpStatus.UNAUTHORIZED), @@ -62,7 +64,15 @@ public enum ErrorCode { MEETING_NOT_FOUND("MEETING_NOT_FOUND", "모임을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), NOT_MEETING_MEMBER("NOT_MEETING_MEMBER", "모임에 가입한 유저가 아닙니다.", HttpStatus.FORBIDDEN), CREATOR_CAN_NOT_LEAVE("CREATOR_CAN_NOT_LEAVE", "작성자는 탈퇴할 수 없습니다.", HttpStatus.BAD_REQUEST), - NOT_EXIST_MY_PAGE("NOT_EXIST_MY_PAGE", "마이페이지를 찾을 수 없습니다.", HttpStatus.NOT_FOUND); + //STARTTIME_CAN_NOT_LATER_THAN_ENDTIME("STARTTIME_CAN_NOT_LATER_THAN_ENDTIME", "d", HttpStatus.BAD_REQUEST), + NOT_BOOKMARKED("NOT_BOOKMARKED", "북마크 하지 않은 모임입니다.",HttpStatus.BAD_REQUEST), + ALREADY_BOOKMARKED("ALREADY_BOOKMARKED", "이미 북마크 된 모임입니다.", HttpStatus.BAD_REQUEST), + BOOKMARK_NOT_FOUND("BOOKMARK_NOT_FOUND", "북마크를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + NOT_EXIST_MY_PAGE("NOT_EXIST_MY_PAGE", "마이페이지를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + KAKAOID_UNLINK_FAILURE("KAKAOID_UNLINK_FAILURE", "카카오 ID 연동에 실패했습니다.", HttpStatus.FAILED_DEPENDENCY), + REFRESH_TOKEN_ERROR("REFRESH_TOKEN_ERROR", "RefreshToken 요청입니다.", HttpStatus.BAD_REQUEST) + ; + diff --git a/src/main/java/com/sparta/moit/global/filter/TimeCheckFilter.java b/src/main/java/com/sparta/moit/global/filter/TimeCheckFilter.java new file mode 100644 index 0000000..cd21228 --- /dev/null +++ b/src/main/java/com/sparta/moit/global/filter/TimeCheckFilter.java @@ -0,0 +1,14 @@ +package com.sparta.moit.global.filter; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; + +public class TimeCheckFilter extends Filter { + @Override + public FilterReply decide(ILoggingEvent event) { + return (event.getLoggerName().equals("com.sparta.moit.global.aspect.PerformanceAspect")) + ? FilterReply.ACCEPT + : FilterReply.DENY; + } +} \ No newline at end of file diff --git a/src/main/java/com/sparta/moit/global/jwt/JwtUtil.java b/src/main/java/com/sparta/moit/global/jwt/JwtUtil.java index fcc3388..60c4cda 100644 --- a/src/main/java/com/sparta/moit/global/jwt/JwtUtil.java +++ b/src/main/java/com/sparta/moit/global/jwt/JwtUtil.java @@ -3,9 +3,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sparta.moit.domain.member.entity.UserRoleEnum; import com.sparta.moit.domain.member.repository.MemberRepository; +import com.sparta.moit.global.common.repository.RedisRefreshTokenRepository; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -15,6 +17,9 @@ import org.springframework.util.StringUtils; import java.io.IOException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Base64; import java.util.Date; @@ -25,24 +30,21 @@ public class JwtUtil { private final MemberRepository memberRepository; + + private final RedisRefreshTokenRepository redisRefreshTokenRepository; public static final String AUTHORIZATION_HEADER = "Authorization"; public static final String AUTHORIZATION_KEY = "auth"; public static final String BEARER_PREFIX = "Bearer "; - private final long TOKEN_TIME = 10 * 24 * 60 * 60 * 1000L; // 60 minutes - - /* refresh token 유효 시간*/ - public static final long REFRESH_TOKEN_VALIDITY_MS = 14 * 24 * 60 * 60 * 1000L; // 14 days - - + private final long TOKEN_TIME = 24 * 60 * 60 * 1000L; // 1 day + private final long REFRESH_TOKEN_TIME = 14 * 24 * 60 * 60 * 1000L; /*14일*/ @Value("${jwt.secret.key}") private String secretKey; - @Value("${jwt.refresh.token.expire.time}") - private long refreshTokenExpireTime; private Key key; private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; - public JwtUtil(MemberRepository memberRepository) { + public JwtUtil(MemberRepository memberRepository, RedisRefreshTokenRepository redisRefreshTokenRepository) { this.memberRepository = memberRepository; + this.redisRefreshTokenRepository = redisRefreshTokenRepository; } @PostConstruct @@ -51,45 +53,13 @@ public void init() { key = Keys.hmacShaKeyFor(bytes); } -// /* Test */ -// public String createTokenForUser(Member user) { -// Date now = new Date(); -// -// return BEARER_PREFIX + -// Jwts.builder() -// .setSubject(user.getEmail()) -// .claim(AUTHORIZATION_KEY, user.getRole()) // 사용자 권한 -// .setExpiration(new Date(now.getTime() + TOKEN_TIME)) -// .setIssuedAt(now) -// .signWith(key, signatureAlgorithm) -// .compact(); -// } -// public String createRefreshTokenForUser(Member user) { -// // 리프레시 토큰 생성 (유니크 토큰 생성 방법) -// String refreshToken = UUID.randomUUID().toString(); -// -// // 리프레시 토큰과 만료 시간을 Member 엔터티에 저장 -// user = Member.builder() -// .id(user.getId()) -// .email(user.getEmail()) -// .password(user.getPassword()) -// .role(user.getRole()) -// .refreshToken(refreshToken) -// .refreshTokenExpiry(new Date(System.currentTimeMillis() + refreshTokenExpireTime)) -// .build(); -// -// // 데이터베이스에 새로운 Member 엔터티 업데이트 (리프레시 토큰과 만료 시간 추가) -// memberRepository.save(user); // 주석 해제하고 memberRepository를 주입하여 사용 -// -// return refreshToken; -// } - public String createToken(String email, UserRoleEnum role) { Date date = new Date(); return BEARER_PREFIX + Jwts.builder() .setSubject(email) + .claim("type", "access") .claim(AUTHORIZATION_KEY, role) // 사용자 권한 .setExpiration(new Date(date.getTime() + TOKEN_TIME)) .setIssuedAt(date) @@ -128,6 +98,15 @@ public boolean validateToken(String token, HttpServletResponse res) throws IOExc } } + public String getTokenType(String token) { + Claims claims = Jwts.parser() + .setSigningKey(secretKey) // secretKey 설정 + .parseClaimsJws(token) // 토큰 파싱 및 검증 + .getBody(); // 토큰의 body (claims) 가져오기 + + return claims.get("type", String.class); + } + private void sendErrorResponse(HttpServletResponse res, int statusCode, String errorMessage) throws IOException { res.setCharacterEncoding("utf-8"); res.setContentType("application/json"); @@ -138,16 +117,67 @@ private void sendErrorResponse(HttpServletResponse res, int statusCode, String e /*리프레시 토큰 생성 메서드*/ public String createRefreshToken(String email, UserRoleEnum role) { - long REFRESH_TOKEN_TIME = 14 * 24 * 60 * 60 * 1000L; /*14일*/ + Date now = new Date(); - return Jwts.builder() + String refresh = Jwts.builder() .setSubject(email) /*사용자 식별자값(ID)*/ .claim(AUTHORIZATION_KEY, role) /*사용자 권한*/ + .claim("type", "refresh") .setExpiration(new Date(now.getTime() + REFRESH_TOKEN_TIME)) /*만료 시간*/ .setIssuedAt(now) /*발급일*/ .signWith(key, signatureAlgorithm) /*암호화 알고리즘*/ .compact(); + +// RedisRefreshToken refreshToken = RedisRefreshToken.builder() +// .token(refresh) +// .email(email) +// .build(); +// +// redisRefreshTokenRepository.save(refreshToken); + + return refresh; + } + + public void addRefreshTokenToCookie(String refreshToken, HttpServletResponse response) { + refreshToken = URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) + .replaceAll("\\+", "%20"); + + Cookie cookie = new Cookie("RefreshToken", refreshToken); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setAttribute("SameSite", "None"); + cookie.setMaxAge((int) (REFRESH_TOKEN_TIME / 1000)); + + response.addCookie(cookie); + } + + public void addAccessTokenToCookie(String accessToken, HttpServletResponse response) { + accessToken = URLEncoder.encode(accessToken, StandardCharsets.UTF_8) + .replaceAll("\\+", "%20"); + + Cookie cookie = new Cookie("AccessToken", accessToken); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + cookie.setAttribute("SameSite", "None"); + cookie.setMaxAge((int) (TOKEN_TIME / 1000)); + + response.addCookie(cookie); + System.out.println(cookie); + } + + public String getRefreshTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("RefreshToken")) { + return URLDecoder.decode(cookie.getValue(), StandardCharsets.UTF_8); + } + } + } + return null; } public Claims getUserInfoFromToken(String token) { diff --git a/src/main/java/com/sparta/moit/global/security/JwtAuthorizationFilter.java b/src/main/java/com/sparta/moit/global/security/JwtAuthorizationFilter.java index deeaae2..c25dc1d 100644 --- a/src/main/java/com/sparta/moit/global/security/JwtAuthorizationFilter.java +++ b/src/main/java/com/sparta/moit/global/security/JwtAuthorizationFilter.java @@ -1,5 +1,7 @@ package com.sparta.moit.global.security; +import com.sparta.moit.global.error.CustomException; +import com.sparta.moit.global.error.ErrorCode; import com.sparta.moit.global.jwt.JwtUtil; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; @@ -7,11 +9,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -32,6 +36,15 @@ public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetail protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException { String tokenValue = jwtUtil.getJwtFromHeader(req); + if (tokenValue == null) { + filterChain.doFilter(req, res); + return; + } + + String tokenType = jwtUtil.getTokenType(tokenValue); + if (!tokenType.equals("access")) { + throw new CustomException(ErrorCode.REFRESH_TOKEN_ERROR); + } if (StringUtils.hasText(tokenValue)) { diff --git a/src/main/java/com/sparta/moit/global/util/AddressUtil.java b/src/main/java/com/sparta/moit/global/util/AddressUtil.java index 10b24e6..c147504 100644 --- a/src/main/java/com/sparta/moit/global/util/AddressUtil.java +++ b/src/main/java/com/sparta/moit/global/util/AddressUtil.java @@ -19,6 +19,8 @@ @Service public class AddressUtil { + + private final RestTemplate restTemplate; public AddressUtil(RestTemplateBuilder builder) { diff --git a/src/main/java/com/sparta/moit/global/util/CareerMapper.java b/src/main/java/com/sparta/moit/global/util/CareerMapper.java new file mode 100644 index 0000000..2d0c0b4 --- /dev/null +++ b/src/main/java/com/sparta/moit/global/util/CareerMapper.java @@ -0,0 +1,43 @@ +package com.sparta.moit.global.util; + +import com.sparta.moit.domain.meeting.dto.CareerDto; +import com.sparta.moit.domain.meeting.dto.CareerResponseDto; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CareerMapper { + + private static final Map CAREER_MAP = Map.ofEntries( + Map.entry(1L, "신입"), + Map.entry(2L, "주니어(1~3)"), + Map.entry(3L, "미들(5~8)"), + Map.entry(4L,"시니어(9~12)"), + Map.entry(5L,"엑스퍼트(13이상)") + ); + + public static List mapCareerIdsToNames(Long[] careerIds) { + return Arrays.stream(careerIds) + .map(CAREER_MAP::get) + .collect(Collectors.toList()); + } + public static List createCareerResponseList(List careerIds) { + return careerIds.stream() + .map(id -> CareerResponseDto.builder() + .careerId(id) + .careerName(CAREER_MAP.get(id)) + .build()) + .collect(Collectors.toList()); + } + public static List createCareerResponseList(Long[] careerIds) { + return Arrays.stream(careerIds) + .map(id -> CareerDto.builder() + .careerId(id) + .careerName(CAREER_MAP.get(id)) + .build()) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/sparta/moit/global/util/PointUtil.java b/src/main/java/com/sparta/moit/global/util/PointUtil.java new file mode 100644 index 0000000..23b994d --- /dev/null +++ b/src/main/java/com/sparta/moit/global/util/PointUtil.java @@ -0,0 +1,14 @@ +package com.sparta.moit.global.util; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; + +public class PointUtil { + public static Point createPointFromLngLat(double longitude, double latitude) { + GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); + return geometryFactory.createPoint(new Coordinate(longitude, latitude)); + } +} + diff --git a/src/main/java/com/sparta/moit/global/util/SkillMapper.java b/src/main/java/com/sparta/moit/global/util/SkillMapper.java new file mode 100644 index 0000000..bf534d1 --- /dev/null +++ b/src/main/java/com/sparta/moit/global/util/SkillMapper.java @@ -0,0 +1,90 @@ +package com.sparta.moit.global.util; + +import com.sparta.moit.domain.meeting.dto.SkillDto; +import com.sparta.moit.domain.meeting.dto.SkillResponseDto; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class SkillMapper { + + private static final Map SKILL_MAP = Map.ofEntries( + Map.entry(1L, "Java"), + Map.entry(2L, "React"), + Map.entry(3L, "Python"), + Map.entry(4L,"JavaScript"), + Map.entry(5L,"Spring"), + Map.entry(6L,"NodeJs"), + Map.entry(7L,"C#"), + Map.entry(8L,"Vue.js"), + Map.entry(9L,"Ruby"), + Map.entry(10L,"Swift"), + Map.entry(11L,"HTML"), + Map.entry(12L,"CSS"), + Map.entry(13L,"SQL"), + Map.entry(14L,"Angular"), + Map.entry(15L,"PHP"), + Map.entry(16L,"Kotlin"), + Map.entry(17L,"Docker"), + Map.entry(18L,"Kubernetes"), + Map.entry(19L,"AWS"), + Map.entry(20L,"GCP"), + Map.entry(21L,"Git"), + Map.entry(22L,"Jenkins"), + Map.entry(23L,"Agile Methodology"), + Map.entry(24L,"React Native"), + Map.entry(25L,"Flutter"), + Map.entry(26L,"TypeScript"), + Map.entry(27L,"AngularJS"), + Map.entry(28L,"Express.js"), + Map.entry(29L,"Flask"), + Map.entry(30L,"Django"), + Map.entry(31L,"Laravel"), + Map.entry(32L,"Ruby"), + Map.entry(33L,"ASP.NET"), + Map.entry(34L,"Unity"), + Map.entry(35L,"Unreal Engine"), + Map.entry(36L,"PostgreSQL"), + Map.entry(37L,"MySQL"), + Map.entry(38L,"MongoDB"), + Map.entry(39L,"Redis"), + Map.entry(40L,"Elasticsearch"), + Map.entry(41L,"GraphQL"), + Map.entry(42L,"Microservices"), + Map.entry(43L,"Serverless Architecture"), + Map.entry(44L,"Blockchain"), + Map.entry(45L,"Machine Learning"), + Map.entry(46L,"Deep Learning"), + Map.entry(47L,"NLP"), + Map.entry(48L,"Data Engineering"), + Map.entry(49L,"DevOps"), + Map.entry(50L,"CI/CD"), + Map.entry(51L,"IaC"), + Map.entry(52L,"C++") + ); + + public static List mapSkillIdsToNames(Long[] skillIds) { + return Arrays.stream(skillIds) + .map(SKILL_MAP::get) + .collect(Collectors.toList()); + } + public static List createSkillResponseList(List skillIds) { + return skillIds.stream() + .map(id -> SkillResponseDto.builder() + .skillId(id) + .skillName(SKILL_MAP.get(id)) + .build()) + .collect(Collectors.toList()); + } + public static List createSkillResponseList(Long[] skillIds) { + return Arrays.stream(skillIds) + .map(id -> SkillDto.builder() + .skillId(id) + .skillName(SKILL_MAP.get(id)) + .build()) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/sparta/moit/global/util/pagination/ListPaginator.java b/src/main/java/com/sparta/moit/global/util/pagination/ListPaginator.java new file mode 100644 index 0000000..9d6a63c --- /dev/null +++ b/src/main/java/com/sparta/moit/global/util/pagination/ListPaginator.java @@ -0,0 +1,10 @@ +package com.sparta.moit.global.util.pagination; + +import java.util.List; + +public class ListPaginator implements Paginator { + @Override + public boolean hasNextPage(List list, int pageSize) { + return list.size() > pageSize; + } +} diff --git a/src/main/java/com/sparta/moit/global/util/pagination/Paginator.java b/src/main/java/com/sparta/moit/global/util/pagination/Paginator.java new file mode 100644 index 0000000..157cddf --- /dev/null +++ b/src/main/java/com/sparta/moit/global/util/pagination/Paginator.java @@ -0,0 +1,7 @@ +package com.sparta.moit.global.util.pagination; + +import java.util.List; + +public interface Paginator { + boolean hasNextPage(List list, int pageSize); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 742540a..7f0e7ec 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,2 @@ -spring.config.import=file:env.properties spring.profiles.active=dev diff --git a/src/main/resources/logback-dev.xml b/src/main/resources/logback-dev.xml new file mode 100644 index 0000000..fdcd6d6 --- /dev/null +++ b/src/main/resources/logback-dev.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + INFO + + + ${CONSOLE_LOG_PATTERN} + + + + + + ${FILE_LOG_PATTERN} + + + ${LOG_PATH}/time-check/time-check.%d{yyyy-MM-dd}_%i.log + + + 10MB + + + 30 + + + + + warn + ACCEPT + DENY + + + ${FILE_LOG_PATTERN} + + + ${LOG_PATH}/warning/warning.%d{yyyy-MM-dd}_%i.log + + 10MB + + 30 + + + + + error + ACCEPT + DENY + + + ${FILE_LOG_PATTERN} + + + ${LOG_PATH}/error/error.%d{yyyy-MM-dd}_%i.log + + 10MB + + 30 + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/sparta/moit/NaverServiceTest.java b/src/test/java/com/sparta/moit/NaverServiceTest.java deleted file mode 100644 index 1252a41..0000000 --- a/src/test/java/com/sparta/moit/NaverServiceTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.sparta.moit; - -import com.sparta.moit.domain.member.repository.MemberRepository; -import com.sparta.moit.domain.member.service.NaverServiceImpl; -import com.sparta.moit.global.jwt.JwtUtil; -import com.sparta.moit.global.common.service.RefreshTokenService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.client.RestTemplate; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -//class NaverServiceTest { -// @Mock -// private RestTemplate restTemplate; -// -// @Mock -// private MemberRepository memberRepository; -// -// @Mock -// private RefreshTokenService refreshTokenService; -// -// @Mock -// private JwtUtil jwtUtil; -// -// @Mock -// private PasswordEncoder passwordEncoder; -// -// @InjectMocks -// private NaverServiceImpl naverService; -// -// @BeforeEach -// void setUp() { -// MockitoAnnotations.openMocks(this); -// } - -// @Test -// void naverLogin_ShouldReturnToken_WhenValidCodeAndStateProvided() throws JsonProcessingException { -// // Given -// String code = "mockCode"; -// String state = "mockState"; -// String refreshToken = "mockRefreshToken"; -// String accessToken = "mockAccessToken"; -// String email = "mockEmail@example.com"; -// String username = "mockUser"; -// Long naverId = 123L; -// Member mockMember = new Member(username, "mockEncodedPassword", email, UserRoleEnum.USER, naverId); -// NaverUserInfoDto naverUserInfoDto = new NaverUserInfoDto(naverId, username, email); -// -// doNothing().when(refreshTokenService).createAndSaveRefreshToken(email, accessToken); -// -// doReturn("mockToken").when(jwtUtil).createToken(username, UserRoleEnum.USER); -// doReturn(new ResponseEntity<>(createNaverAccessTokenResponseJson(accessToken, refreshToken), HttpStatus.OK)) -// .when(restTemplate).exchange(any(RequestEntity.class), eq(String.class)); -// doReturn(new ResponseEntity<>(createNaverUserInfoResponseJson(naverId, username, email), HttpStatus.OK)) -// .when(restTemplate).exchange(any(RequestEntity.class), eq(String.class)); -// doReturn(Optional.empty()).when(memberRepository).findByNaverId(naverId); -// doReturn(Optional.empty()).when(memberRepository).findByEmail(email); -// doReturn("mockEncodedPassword").when(passwordEncoder).encode(anyString()); -// // When -// String token = naverService.naverLogin(code, state, refreshToken); -// -// // Then -// assertNotNull(token); -// assertEquals("mockToken", token); -// verify(refreshTokenService, times(1)).createAndSaveRefreshToken(email, accessToken); -// verify(jwtUtil, times(1)).createToken(username, UserRoleEnum.USER); -// verify(memberRepository, times(1)).findByNaverId(naverId); -// verify(memberRepository, times(1)).findByEmail(email); -// verify(memberRepository, times(1)).save(mockMember); -// } -// -// @Test -// void refreshToken_ShouldReturnNewAccessToken_WhenValidRefreshTokenProvided() { -// // Given -// String refreshToken = "mockRefreshToken"; -// String newAccessToken = "mockNewAccessToken"; -// -// when(refreshTokenService.refreshAccessToken(refreshToken)).thenReturn(Optional.of(newAccessToken)); -// -// // When -// String accessToken = naverService.refreshToken(refreshToken); -// -// // Then -// assertEquals(newAccessToken, accessToken); -// } -// -// @Test -// void naverLogout_ShouldDeleteRefreshToken_WhenValidRefreshTokenProvided() { -// // Given -// String refreshToken = "mockRefreshToken"; -// -// // When -// naverService.logout(refreshToken); -// -// // Then -// verify(refreshTokenService, times(1)).deleteRefreshToken(refreshToken); -// } -// } \ No newline at end of file