diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1644596c..f9172738 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -59,10 +59,7 @@ jobs: runs-on: self-hosted # weekly/* 브랜치에서는 실행되지 않도록 조건 추가 - # if: startsWith(github.ref, 'refs/heads/Master') || startsWith(github.ref, 'refs/heads/develop') - - # weekly/* 브랜치에서만 실행되도록 - if: startsWith(github.ref, 'refs/heads/weekly/') + if: startsWith(github.ref, 'refs/heads/Master') || startsWith(github.ref, 'refs/heads/develop') steps: # 최신 이미지를 pull @@ -97,13 +94,6 @@ jobs: echo "next_version=${NEXT_VERSION}" >> $GITHUB_ENV echo "port=${PORT}" >> $GITHUB_ENV - # 기존의 동일한 컨테이너를 중지 (블루/그린 중 다음 배포 타겟에 해당하는 컨테이너) - - name: Stop current docker container - run: | - echo "Stopping container inplace-${{ env.next_version }}" - sudo docker stop inplace-${{ env.next_version }} || true - sudo docker rm inplace-${{ env.next_version }} || true - # .env 파일 생성 - name: Create .env file run: | @@ -116,8 +106,27 @@ jobs: echo "Running new container inplace-${{ env.next_version }}" sudo docker run --name "inplace-${{ env.next_version }}" --rm -d -p ${{ env.port }}:8080 --env-file .env ${{ secrets.DOCKERHUB_USERNAME }}/inplace - # 로드 밸런서 타겟 그룹을 업데이트하여 트래픽을 새 컨테이너로 전환합니다. + # 스프링부트 시작을 기다리기 위해 헬스 체크로 대기 + - name: Wait for Spring Boot Application to Start + run: | + echo "Waiting for the application to be healthy on PORT: ${{ env.port }}..." + for i in {1..40}; do + if curl -s http://localhost:${{ env.port }}/actuator/health | grep '"status":"UP"' > /dev/null; then + echo "Application is healthy" + break + fi + echo "Waiting for new application to start...($i)" + sleep 5 + done + + if [ $i -eq 40 ]; then + echo "Application failed to start after 40 attempts." + exit 1 + fi + + # 로드 밸런서 타겟 그룹을 업데이트하여 트래픽을 새 컨테이너로 전환 - name: Update Load Balancer Target Group + if: success() run: | TARGET_GROUP_ARN_BLUE="${{ secrets.TARGET_GROUP_ARN_8080 }}" TARGET_GROUP_ARN_GREEN="${{ secrets.TARGET_GROUP_ARN_8081 }}" @@ -127,6 +136,14 @@ jobs: aws elbv2 modify-listener --listener-arn ${{ secrets.LISTENER_ARN_443 }} --default-actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN_GREEN fi + # 이전 컨테이너 정리 전 드레이닝 시간 부여 + - name: Wait for connection draining + run: | + if [[ "${{ env.current_version }}" != "none" ]]; then + echo "Waiting 30 seconds for connection draining..." + sleep 30 + fi + # 이전 컨테이너 정리 - name: Remove previous docker container run: | diff --git a/README.md b/README.md index b52c56a7..ca71f403 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,814 @@ -# Team7_BE +# Inplace ( Team7_BE ) -## Project Version +

+ 메인 이미지 +

-- Spring Boots 3.3.3 -- Java 17 LTS \ No newline at end of file +# 목차 + +1. [**프로젝트 개요**](#-프로젝트-개요) + > 1.1 [**프로젝트 목적**](#-프로젝트-목적) + > + > 1.2 [**서비스 정보**](#-서비스-정보) + > + > 1.3 [**배포 주소**](#-배포-주소) + +2. [**프로젝트 구조**](#-프로젝트-구조) + > 2.1 [**프로그램 구조도**](#-프로그램-구조도) + > + > 2.2 [**API & ERD**](#-api--erd) + > + > 2.3 [**패키지 구조**](#패키지-구조) + +3. [**개발 정보**](#-개발-정보) + > 3.1 [**개발 기간**](#-개발-기간) + > + > 3.2 [**팀원**](#-팀원) + > + > 3.3 [**컨벤션**](#컨벤션) + > + > 3.4 [**브랜치 전략**](#브랜치-전략) + > + > 3.5 [**저장소 정보**](#-저장소-정보) + +4. [**기술 정보**](#-기술-정보) + > 4.1 [**주요 종속성 버전**](#-주요-종속성-버전) + > + > 4.2 [**기술 스택**](#-기술-스택) + +5. [**테스트**](#-테스트) + +6. [**기능 및 사용 예시**](#-기능-및-사용-예시) + > 6.1 [**세부 기능 흐름**](#-세부-기능-흐름) + > + > 6.2 [**사용 예시**](#-사용-예시) + +--- + +# 📝 프로젝트 개요 + +## 🙌 프로젝트 목적 + +> 저희의 아이디어는 **데이트 코스의 단조로움**을 어떻게 하면 해소할 수 있을까? 💡 라는 생각에서 시작했습니다. +> +> 아이디어를 구체화하는 과정에서 저희는 **장소**에 관련된 소재로 인스타, 유튜브 등의 SNS 및 동영상 플랫폼 📱의 성장과 함께 등장한 **인플루언서** 라는 개념에 집중하게 +> 되었습니다. +> +> 이는 **인플루언서가 방문한 장소에 대한 정보를 서비스 해보자!** 📍 라는 생각으로 이어졌습니다. +> +> 이를 통해 사용자가 관심있는 **인플루언서를 등록하고, 이에 따른 장소 추천 및, 장소에 대한 리뷰 기능** 📋을 제공하여, **Inplace** 라는 저희만의 웹 +> 애플리케이션으로 구현해보았습니다. + +## 📋 서비스 정보 + +> 긴 영상은 필요 없어요 인플루언서가 다녀간 쿨플, 한눈에 쏙! + +1. **회원가입 및 로그인** + - Spring Security를 이용한 OAuth 2.0 카카오 로그인 기능을 사용합니다 + - 쿠키에 Access, Refresh Token을 담아 사용하며, Refresh 동작을 수행할 수 있습니다 +2. **현재 위치 기반 서비스** + - 웹 페이지의 위치 정보 사용에 동의시, 카카오 API와 내 위치 정보를 사용하여 주변의 장소 정보와, 자동 지도 위치 설정을 사용할 수 있습니다 +3. **통합 검색 서비스** + - Elastic Search를 사용한 인플루언서, 장소, 비디오 이름에 대한 통합 검색 기능을 사용할 수 있습니다 +4. **관심 인플루언서 등록 및 이를 토대로 한 서비스** + - 최초 로그인 시 & 인플루언서 페이지에서 관심 인플루언서를 등록할 수 있습니다 + - 이를 토대로 메인 페이지에서 관심 인플루언서의 최신 방문 장소를 확인할 수 있습니다 +5. **지도 기반 검색 서비스** + - 지도 API를 이용하여 장소를 검색할 수 있습니다 + - 관심 등록하지 않은 인플루언서 및 주소, 장소 태그를 이용하여 세부 검색이 가능합니다 +6. **장소 세부 정보 서비스** + - 장소의 세부 정보를 열람할 수 있습니다 + - 장소에 달린 다른 유저들의 리뷰를 확인할 수 있습니다 + - 장소에 대한 좋아요 기능을 사용할 수 있습니다 + - 장소 세부 페이지에서 장소에 대한 정보를 카카오톡 메세지로 받아 볼 수 있습니다 +7. **리뷰 기능** + - 장소 정보를 받은 후 3일 뒤, 해당 장소에 대한 리뷰 링크를 받아 리뷰를 작성할 수 있습니다 + - 위 기능들은 카카오톡 메세지 보내기 API를 이용하며, 카카오톡으로 전송되는 링크는 모바일 뷰를 지원합니다 +8. **마이 페이지 기능** + - 좋아요 표시한 장소, 인플루언서를 관리할 수 있습니다 + - 내가 작성한 리뷰를 관리할 수 있습니다 + - 사용자 닉네임을 변경할 수 있습니다 + +## 🌐 배포 주소 + +> **BackEnd** : [**_api.inplace.my_**](https://api.inplace.my) +> +> **FrontEnd**: [**_inplace.my_**](https://inplace.my) + +--- + +# 🏗️ 프로젝트 구조 + +## 🖼️ 프로그램 구조도 + +### CI CD + +![Backend_CICD](https://github.com/user-attachments/assets/f7db576e-5150-443c-8d2f-ab232294c296) + +### System Architecture + +![Architecture](https://github.com/user-attachments/assets/b21a697a-003e-4371-bdde-e12db6dbc1d9) + +## 📊 API & ERD + +![image](https://github.com/user-attachments/assets/ce11ff39-8294-4eda-aa58-7b5bd0ac1620) + +- [🚗 Visit Team7 API](https://www.notion.so/API-9e96d1ef1475414b861a50d0e4ca366e) + +![image](https://github.com/user-attachments/assets/333c9f3b-6678-48a6-b32d-b9b36d8cb182) + +- [🙋‍♂️ Visit Team7 ERD](https://www.notion.so/ERD-36ec8e40cb264abe87588e97ae77ac55) + +## 🗂️패키지 구조 + +
패키지 구조 + + +``` +📦src + ┣ 📂main + ┃ ┣ 📂generated + ┃ ┃ ┗ 📂team7 + ┃ ┃ ┃ ┗ 📂inplace + ┃ ┃ ┃ ┃ ┣ 📂crawling + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QYoutubeChannel.java + ┃ ┃ ┃ ┃ ┣ 📂favoriteInfluencer + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QFavoriteInfluencer.java + ┃ ┃ ┃ ┃ ┣ 📂global + ┃ ┃ ┃ ┃ ┃ ┗ 📂exception + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QErrorLog.java + ┃ ┃ ┃ ┃ ┣ 📂influencer + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QInfluencer.java + ┃ ┃ ┃ ┃ ┣ 📂likedPlace + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QLikedPlace.java + ┃ ┃ ┃ ┃ ┣ 📂oauthToken + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QOauthToken.java + ┃ ┃ ┃ ┃ ┣ 📂place + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜QAddress.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜QCoordinate.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜QMenu.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜QOffDay.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜QOpenTime.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QPlace.java + ┃ ┃ ┃ ┃ ┣ 📂review + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QReview.java + ┃ ┃ ┃ ┃ ┣ 📂user + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QUser.java + ┃ ┃ ┃ ┃ ┗ 📂video + ┃ ┃ ┃ ┃ ┃ ┗ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QVideo.java + ┃ ┣ 📂java + ┃ ┃ ┗ 📂team7 + ┃ ┃ ┃ ┗ 📂inplace + ┃ ┃ ┃ ┃ ┣ 📂admin + ┃ ┃ ┃ ┃ ┃ ┣ 📂banner + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂command + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜BannerCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜BannerInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜BannerService.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜Banner.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜BannerRepository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜BannerS3Repository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜BannerController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜BannerRequest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜BannerResponse.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂cicd + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜TestController.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂crawling + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜CrawlingInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AddressUtil.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CrawlingFacade.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜KakaoCrawlingService.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜VideoCrawlingService.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜YoutubeCrawlingService.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂client + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceNode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜KakaoMapClient.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜YoutubeClient.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ChannelType.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜YoutubeChannel.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜YoutubeChannelRepository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜CrawlingController.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂error + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ErrorLog.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ErrorLogController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ErrorLogRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📜AdminPageController.java + ┃ ┃ ┃ ┃ ┣ 📂favoriteInfluencer + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜FavoriteInfluencerCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜FavoriteInfluencerListCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜FavoriteInfluencerService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜FavoriteInfluencer.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistent + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜FavoriteInfluencerRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerLikeRequest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerListLikeRequest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜FavoriteInfluencerController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜FavoriteInfluencerControllerApiSpec.java + ┃ ┃ ┃ ┃ ┣ 📂global + ┃ ┃ ┃ ┃ ┃ ┣ 📂annotation + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜Facade.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂exception + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂code + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AuthorizationErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜BannerErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ChannelErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜LikedPlaceErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ReviewErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜UserErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoErrorCode.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InplaceException.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InplaceExceptionHandler.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂kakao + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📂config + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜KakaoApiProperties.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂queryDsl + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜QueryDslConfig.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂rest + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜RestTemplateConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜WebClientConfig.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂scheduler + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ScheduledExecutorConfig.java + ┃ ┃ ┃ ┃ ┣ 📂influencer + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerNameInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜Influencer.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerNameResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerRequest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerControllerApiSpec.java + ┃ ┃ ┃ ┃ ┣ 📂infra + ┃ ┃ ┃ ┃ ┃ ┗ 📂s3 + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AwsProperties.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜S3Config.java + ┃ ┃ ┃ ┃ ┣ 📂likedPlace + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜LikedPlace.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜LikedPlaceRepository.java + ┃ ┃ ┃ ┃ ┣ 📂oauthToken + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂command + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜OauthTokenCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜OauthTokenService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜OauthToken.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜OauthTokenRepository.java + ┃ ┃ ┃ ┃ ┣ 📂place + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂command + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceLikeCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlacesCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CategoryInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜LikedPlaceInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceDetailInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceForVideo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CategoryService.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Address.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Category.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Coordinate.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Menu.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜OffDay.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜OpenTime.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜Place.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceCustomRepository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceCustomRepositoryImpl.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CategoriesResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceDetailResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceLikeRequest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlacesResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ReviewRequest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceControllerApiSpec.java + ┃ ┃ ┃ ┃ ┣ 📂placeMessage + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂command + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceMessageCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜KakaoMessageService.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceMessageFacade.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceMessageController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceMessageControllerApiSpec.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂util + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Button.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Content.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜FeedTemplate.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜KakaoMessageMaker.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Link.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜LocationTemplate.java + ┃ ┃ ┃ ┃ ┣ 📂review + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜MyReviewInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ReviewCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜Review.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ReviewController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewControllerApiSpec.java + ┃ ┃ ┃ ┃ ┣ 📂search + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AutoCompletionInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceSearchInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜SearchType.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜SearchService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂config + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜MatchAgainstFunctionContributor.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜SearchResult.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerSearchRepository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜PlaceSearchRepository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SearchRepository.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoSearchRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SearchController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜SearchControllerApiSpec.java + ┃ ┃ ┃ ┃ ┣ 📂security + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CustomOAuth2User.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜KakaoOAuthResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CurrentUserProvider.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜CustomOAuth2UserService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂config + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CorsConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜JwtProperties.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜RedisConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SecurityConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SecurityEntryPointConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SecurityFilterConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SecurityHandlerConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜SecurityServiceConfig.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜SecurityUtilConfig.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂entryPoint + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜LoginAuthenticationEntryPoint.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂filter + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AuthorizationFilter.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜ExceptionHandlingFilter.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜TokenType.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂handler + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CustomAccessDeniedHandler.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CustomFailureHandler.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜CustomSuccessHandler.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂util + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AuthorizationUtil.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CookieUtil.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜JwtUtil.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜TokenEncryptionUtil.java + ┃ ┃ ┃ ┃ ┣ 📂token + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜TokenCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜RefreshTokenFacade.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜RefreshTokenService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜RefreshToken.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜RefreshTokenRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜RefreshTokenController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜RefreshTokenControllerApiSpec.java + ┃ ┃ ┃ ┃ ┣ 📂user + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜UserCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜UserInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜UserFacade.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜UserService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Role.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜User.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜UserType.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜UserRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜LikedInfluencerResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜LikedPlaceResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜MyReviewResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜UserInfoResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜TempController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜UserController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜UserControllerApiSepc.java + ┃ ┃ ┃ ┃ ┣ 📂video + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂command + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoCommand.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoInfo.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AliasUtil.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜Template.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜VideoFacade.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoService.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜Video.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoRepository.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂presentation + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📂dto + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜VideoResponse.java + ┃ ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoSearchParams.java + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜VideoController.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoControllerApiSpec.java + ┃ ┃ ┃ ┃ ┗ 📜InplaceApplication.java + ┃ ┗ 📂resources + ┃ ┃ ┣ 📂META-INF + ┃ ┃ ┃ ┗ 📂services + ┃ ┃ ┃ ┃ ┗ 📜org.hibernate.boot.model.FunctionContributor + ┃ ┃ ┣ 📂sql + ┃ ┃ ┃ ┣ 📜data.sql + ┃ ┃ ┃ ┗ 📜schema.sql + ┃ ┃ ┣ 📂static + ┃ ┃ ┃ ┣ 📂css + ┃ ┃ ┃ ┃ ┣ 📜banner.css + ┃ ┃ ┃ ┃ ┣ 📜error_logs_style.css + ┃ ┃ ┃ ┃ ┣ 📜main.css + ┃ ┃ ┃ ┃ ┗ 📜style.css + ┃ ┃ ┃ ┣ 📂js + ┃ ┃ ┃ ┃ ┣ 📜banner.js + ┃ ┃ ┃ ┃ ┣ 📜error-logs.js + ┃ ┃ ┃ ┃ ┣ 📜main.js + ┃ ┃ ┃ ┃ ┗ 📜video.js + ┃ ┃ ┃ ┗ 📜favicon.ico + ┃ ┃ ┣ 📂templates + ┃ ┃ ┃ ┣ 📂admin + ┃ ┃ ┃ ┃ ┣ 📜banner.html + ┃ ┃ ┃ ┃ ┣ 📜error-logs.html + ┃ ┃ ┃ ┃ ┣ 📜main.html + ┃ ┃ ┃ ┃ ┗ 📜video.html + ┃ ┃ ┃ ┗ 📂introduce + ┃ ┃ ┃ ┃ ┗ 📜main.html + ┃ ┃ ┣ 📜application-db.yaml + ┃ ┃ ┣ 📜application-kakao.yaml + ┃ ┃ ┣ 📜application-redis.yaml + ┃ ┃ ┣ 📜application-s3.yaml + ┃ ┃ ┣ 📜application-security.yaml + ┃ ┃ ┣ 📜application-youtube.yaml + ┃ ┃ ┗ 📜application.yaml + ┗ 📂test + ┃ ┗ 📂java + ┃ ┃ ┗ 📂team7 + ┃ ┃ ┃ ┗ 📂inplace + ┃ ┃ ┃ ┃ ┣ 📂config + ┃ ┃ ┃ ┃ ┃ ┗ 📂annotation + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜CustomRepositoryTest.java + ┃ ┃ ┃ ┃ ┣ 📂crawling + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜AddressUtilTest.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂client + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜KakaoMapClientTest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜YoutubeClientTest.java + ┃ ┃ ┃ ┃ ┣ 📂influencer + ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerControllerTest.java + ┃ ┃ ┃ ┃ ┃ ┣ 📜InfluencerRepositoryTest.java + ┃ ┃ ┃ ┃ ┃ ┗ 📜InfluencerServiceTest.java + ┃ ┃ ┃ ┃ ┣ 📂likedPlace + ┃ ┃ ┃ ┃ ┃ ┗ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜LikedPlaceRepositoryTest.java + ┃ ┃ ┃ ┃ ┣ 📂place + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceServiceTest.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜PlaceRepositoryTest.java + ┃ ┃ ┃ ┃ ┣ 📂review + ┃ ┃ ┃ ┃ ┃ ┣ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewRepositoryTest.java + ┃ ┃ ┃ ┃ ┃ ┗ 📜ReviewServiceTest.java + ┃ ┃ ┃ ┃ ┣ 📂security + ┃ ┃ ┃ ┃ ┃ ┣ 📂config + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜CorsTest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜SecurityConfigTest.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂util + ┃ ┃ ┃ ┃ ┃ ┃ ┣ 📜AuthorizationUtilTest.java + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜JwtUtilTest.java + ┃ ┃ ┃ ┃ ┣ 📂util + ┃ ┃ ┃ ┃ ┃ ┗ 📜TestUtil.java + ┃ ┃ ┃ ┃ ┣ 📂video + ┃ ┃ ┃ ┃ ┃ ┣ 📂application + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoServiceTest.java + ┃ ┃ ┃ ┃ ┃ ┣ 📂domain + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoTest.java + ┃ ┃ ┃ ┃ ┃ ┗ 📂persistence + ┃ ┃ ┃ ┃ ┃ ┃ ┗ 📜VideoRepositoryTest.java + ┃ ┃ ┃ ┃ ┗ 📜InplaceApplicationTests.java +``` + +
+ +--- + +# 👨‍💻 개발 정보 + +## 📅 개발 기간 + +> **_2024.08.22 ~ 2024.11.15_** + +## 👥 팀원 + + + + + + + + + + + +
+ Frontend
+
+ Frontend
+
+
이정민
+
+
이효은
+
+ + + + + + + + + + + + + + + +
+ Backend
+
+ Backend
+
+ Backend
+
+ Backend
+
+ Backend
+
+
이상희
+
+
김동윤
+
+
정수현
+
+
우현서
+
+
배준호
+
+ + +## 컨벤션 + +> [**Commit Convention +**](https://github.com/kakao-tech-campus-2nd-step3/Team7_BE/wiki/Commit-%EC%BB%A8%EB%B2%A4%EC%85%98) +> +> [**PR Convention**](https://github.com/kakao-tech-campus-2nd-step3/Team7_BE/wiki/PR-%EC%BB%A8%EB%B2%A4%EC%85%98) +> +> [**Issue Convention +**](https://github.com/kakao-tech-campus-2nd-step3/Team7_BE/wiki/%EC%9D%B4%EC%8A%88-%EC%BB%A8%EB%B2%A4%EC%85%98) +> +> [**Coding Convention +**](https://github.com/kakao-tech-campus-2nd-step3/Team7_BE/wiki/%EC%BD%94%EB%94%A9-%EC%BB%A8%EB%B2%A4%EC%85%98) +> +> [**브랜치 전략 +**](https://github.com/kakao-tech-campus-2nd-step3/Team7_BE/wiki/%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5) +> +💡 *컨벤션과 +브랜치전략은 [Notiion](https://quickest-asterisk-75d.notion.site/7-7-05d799c3f023443f8fb66abf97ca5965) +에서도 확인이 가능합니다!* + +## 📂 저장소 정보 + +> **Using Language** +> +![GitHub language count](https://img.shields.io/github/languages/count/kakao-tech-campus-2nd-step3/Team7_BE) +![GitHub top language](https://img.shields.io/github/languages/top/kakao-tech-campus-2nd-step3/Team7_BE) + +> **Repo, Code Volume** +> +![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/kakao-tech-campus-2nd-step3/Team7_BE) +![GitHub repo size](https://img.shields.io/github/repo-size/kakao-tech-campus-2nd-step3/Team7_BE) + +> **Commit Avg** +> +![GitHub commit activity](https://img.shields.io/github/commit-activity/w/kakao-tech-campus-2nd-step3/Team7_BE) + +> **Issues** +> +![GitHub open issues](https://img.shields.io/github/issues/kakao-tech-campus-2nd-step3/Team7_BE) +![GitHub closed issues](https://img.shields.io/github/issues-closed/kakao-tech-campus-2nd-step3/Team7_BE) + +> **PRs** +> +![GitHub pull requests](https://img.shields.io/github/issues-pr/kakao-tech-campus-2nd-step3/Team7_BE?label=open%20pull%20requests) +![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/kakao-tech-campus-2nd-step3/Team7_BE?label=closed%20pull%20requests) + +--- + +# 🛠️ 기술 정보 + +## 🧩 주요 종속성 버전 + +> **Spring Boots 3.3.3** +> +> **Java 17 LTS** +> +> **AWS** +> Spring-Cloud-Starter-AWS:2.2.6.RELEASE +> +> **JWT** +> +> JJWT-API:0.12.3 +> +> JJWT-IMPL:0.12.3 +> +> JJWT-JACKSON:0.12.3 +> +> **SpringDoc(Swagger)** +> +> SpringDoc-OpenApi-Starter-WebMvc-UI: 2.0.3 +> +> **QueryDSL** +> +> QueryDsl-JPA: 5.0.0(Jakarata) +> +> **Spring Starter는 Spring버전을 따릅니다** +> +> spring-boot-starter-security +> +> spring-boot-starter-oauth2-client +> +> spring-boot-starter-data-jpa +> +> spring-boot-starter-data-redis +> +> spring-boot-starter-thymeleaf + +## 🚀 기술 스택 + +> **Backend** +> +![Spring Boot](https://img.shields.io/badge/Spring%20Boot-6DB33F?style=flat-square&logo=springboot&logoColor=white) +![Lombok](https://img.shields.io/badge/Lombok-DC382D?style=flat-square&logo=lombok&logoColor=white) + +> **Security** +> +![Spring Security](https://img.shields.io/badge/Spring%20Security-6DB33F?style=flat-square&logo=springsecurity&logoColor=white) +![JWT](https://img.shields.io/badge/JWT-000000?style=flat-square&logo=jsonwebtokens&logoColor=white) + +> **DB** +> +![Spring Data JPA](https://img.shields.io/badge/Spring%20Data%20JPA-6DB33F?style=flat-square&logo=spring&logoColor=white) +![QueryDSL](https://img.shields.io/badge/QueryDSL-0055a2?style=flat-square&logo=appveyor&logoColor=white) +![MySQL](https://img.shields.io/badge/MySQL-4479A1?style=flat-square&logo=mysql&logoColor=white) +![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat-square&logo=redis&logoColor=white) + +> **Web** +> +![Spring WebFlux](https://img.shields.io/badge/Spring%20WebFlux-6DB33F?style=flat-square&logo=spring&logoColor=white) + +> **Deployment** +> +![AWS EC2](https://img.shields.io/badge/AWS%20EC2-FF9900?style=flat-square&logo=amazonaws&logoColor=white) +![Docker](https://img.shields.io/badge/Docker-2496ED?style=flat-square&logo=docker&logoColor=white) + +> **Admin Page** +> +![jQuery](https://img.shields.io/badge/jQuery-0769AD?style=flat-square&logo=jquery&logoColor=white) +![Thymeleaf](https://img.shields.io/badge/Thymeleaf-005F0F?style=flat-square&logo=thymeleaf&logoColor=white) +![AJAX](https://img.shields.io/badge/AJAX-005571?style=flat-square&logo=ajax&logoColor=white) + +> **Test** +> +![JMeter](https://img.shields.io/badge/JMeter-D22128?style=flat-square&logo=apachejmeter&logoColor=white) +![JUnit](https://img.shields.io/badge/JUnit-25A162?style=flat-square&logo=junit5&logoColor=white) +![Mockito](https://img.shields.io/badge/Mockito-FFCA28?style=flat-square&logo=mockito&logoColor=white) + +> **Code Maintenance** +> +![Git](https://img.shields.io/badge/Git-F05032?style=flat-square&logo=git&logoColor=white) +![GitHub](https://img.shields.io/badge/GitHub-181717?style=flat-square&logo=github&logoColor=white) +![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-2088FF?style=flat-square&logo=githubactions&logoColor=white) + +> **Collaboration Tool** +> +![Discord](https://img.shields.io/badge/Discord-5865F2?style=flat-square&logo=discord&logoColor=white) +![Slack](https://img.shields.io/badge/Slack-4A154B?style=flat-square&logo=slack&logoColor=white) + +--- + +# ✅ 테스트 + +> **테스트 시나리오** : https://www.notion.so/9ed68b292c004fc69f7eaad513054d96 +> +> **테스트 결과보고서** : https://www.notion.so/08a520d3b8c44154a19425b0bcc16f6f + +--- + +# 🎬 기능 및 사용 예시 + +## 🔍 세부 기능 흐름 + +- ### Spring Security + - oauth 로그인 시, jwt로 accessToken과 refreshToken을 Cookie에 담아줍니다. + - oauthToken은 aes알고리즘으로 암호화 되어 db에 저장됩니다. + - 모든 요청은 AuthorizationFilter에서 Cookie에 있는 토큰이 유효한지 확인하고, 유효하면 Authenticate합니다. +- ### Influencer + - 로그인 상태인 경우 사용자가 좋아요한 인플루언서를 먼저 반환합니다. +- ### FavoriteInfluencer + - 로그인 상태를 확인한 후 좋아요/ 싫어요 요청을 처리합니다. + - 내가 좋아요한 인플루언서 정보를 반환합니다. +- ### Place + - places에 videos, influecers 테이블을 Left join하고 지도 범위 내에있는 장소를 추려냅니다. + - 카테고리와 인플루언서 이름으로 필터링하고, 사용자와 가까운순으로 정렬 후 반환합니다. + - 특정 장소에 대한 요청이 들어오면 세부 정보와 함께 관련된 인플루언서, 비디오와 사용자 리뷰 정보를 추가하여 반환합니다. + - 내가 좋아요한 장소 조회 시 인플루언서 정보 일부를 함께 반환합니다. + - PlaceMessage + - 요청이 오면 webClient(비동기)로 나에게보내기 kakao api를 통해 장소 정보를 보냅니다. + - 3일 뒤 나에게 보내기 kakao api를 통해 리뷰 요청 메세지를 보냅니다. +- ### LikedPlace + - 사용자가 장소에 좋아요를 누르거나 취소하면 상태를 업데이트합니다. + - 기존 정보가 존재하지 않으면 새로 생성후 저장합니다. +- ### Video + - 로그인 상태인 경우 사용자가 좋아요한 인플루언서의 동영상, 사용자 주변 장소의 동영상을 반환합니다. + - 로그인 상태가 아닌 경우 조회수 증가량이 높은 동영상, 새로운 동영상을 반환합니다. +- ### Review + - 로그인 상태를 확인한 후 리뷰 추가, 리뷰 삭제 요청을 처리합니다. + - 장소별 리뷰 조회 시 본인이 작성한 리뷰인지 여부를 포함하여 반환합니다. + - 내가 작성한 리뷰 조회 시 장소 정보 일부를 함께 반환합니다. +- ### User + - User nickname을 더티 체킹을 통해 변경한다. +- ### TokenRefresh + - Redis DB에 username(key)로 refreshToken을 확인하고, RTR (Refresh Token Rotation)을 합니다. +- ### videoCrawling + - DB에서 인플루언서 유튜브 채널을 가져와 새로 업데이트 된 정보 크롤링해서 정규표현식으로 주소정보를 추출합니다. + - Video에서 장소정보가 추출되면 kakaoApi를 통해 매장 정보를 가져와 인플루언서별로 트랜잭션을 분리해 장소정보를 저장합니다. + - Video에서 장소정보가 추출되지 않으면 Admin페이지를 통해 수동으로 장소 정보를 입력합니다. +- ### AdminPage + - 배너, 에러로그, 강제크롤링 기능을 제공합니다. + - 홈페이지에 배너를 등록하거나 삭제할 수 있습니다. + - 서버 내에서 발생한 500번대 에러로그와 StackTrace를 확인할 수 있습니다. + - 스케쥴링 되어있는 비디오 크롤링을 강제로 실행할 수 있습니다. + - 스케쥴링 되어있는 조회수 크롤링을 강제로 실행할 수 있습니다. + +## 📱 사용 예시 + +> 실제 유저 사용 페이지 흐름 보여주기 ( 영상 x ) diff --git a/build.gradle b/build.gradle index 37c3cf6d..c1f7de09 100644 --- a/build.gradle +++ b/build.gradle @@ -28,9 +28,11 @@ dependencies { //Spring implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' testImplementation 'io.projectreactor:reactor-test' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-actuator' //Security implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/prod.env b/prod.env new file mode 100644 index 00000000..efdecbd5 --- /dev/null +++ b/prod.env @@ -0,0 +1,17 @@ +KAKAO_CLIENT_ID=059dc1f42bbce3a6499c8c35e806b59e +KAKAO_CLIENT_SECRET=XhG4I7doMSAPynuSj3YeCUJfHMGflQZ7 +KAKAO_REDIRECT_URI={baseUrl}/login/oauth2/code/kakao + +JWT_SECRET=1234567890 +JWT_ACCESS_TOKEN_EXPIRED_TIME=300000 +JWT_REFRESH_TOKEN_EXPIRED_TIME=60 + +YOUTUBE_API_KEY=AIzaSyCVxk43oKFnjbMacGK7m9MqPzsU5-736Ok +KAKAO_REST_API_KEY=89fb1518340b58930403f620b14e697d + +DATABASE_URL=jdbc:mysql://inplace-database.cnqma68ee3ul.ap-northeast-2.rds.amazonaws.com:3306/inplace +DATABASE_USERNAME=admin +DATABASE_PASSWORD=tanghulu123! + +REDIS_DATABASE_URL=localhost +REDIS_DATABASE_PORT=6379 \ No newline at end of file diff --git a/src/main/generated/team7/inplace/admin/banner/domain/QBanner.java b/src/main/generated/team7/inplace/admin/banner/domain/QBanner.java new file mode 100644 index 00000000..64578408 --- /dev/null +++ b/src/main/generated/team7/inplace/admin/banner/domain/QBanner.java @@ -0,0 +1,47 @@ +package team7.inplace.admin.banner.domain; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QBanner is a Querydsl query type for Banner + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBanner extends EntityPathBase { + + private static final long serialVersionUID = -455211757L; + + public static final QBanner banner = new QBanner("banner"); + + public final DateTimePath endDate = createDateTime("endDate", java.time.LocalDateTime.class); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath imgName = createString("imgName"); + + public final StringPath imgPath = createString("imgPath"); + + public final BooleanPath isFixed = createBoolean("isFixed"); + + public final DateTimePath startDate = createDateTime("startDate", java.time.LocalDateTime.class); + + public QBanner(String variable) { + super(Banner.class, forVariable(variable)); + } + + public QBanner(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QBanner(PathMetadata metadata) { + super(Banner.class, metadata); + } + +} + diff --git a/src/main/generated/team7/inplace/crawling/domain/QYoutubeChannel.java b/src/main/generated/team7/inplace/admin/crawling/domain/QYoutubeChannel.java similarity index 92% rename from src/main/generated/team7/inplace/crawling/domain/QYoutubeChannel.java rename to src/main/generated/team7/inplace/admin/crawling/domain/QYoutubeChannel.java index 7b379620..9bba9349 100644 --- a/src/main/generated/team7/inplace/crawling/domain/QYoutubeChannel.java +++ b/src/main/generated/team7/inplace/admin/crawling/domain/QYoutubeChannel.java @@ -1,4 +1,4 @@ -package team7.inplace.crawling.domain; +package team7.inplace.admin.crawling.domain; import static com.querydsl.core.types.PathMetadataFactory.*; @@ -15,7 +15,7 @@ @Generated("com.querydsl.codegen.DefaultEntitySerializer") public class QYoutubeChannel extends EntityPathBase { - private static final long serialVersionUID = 2131098359L; + private static final long serialVersionUID = -73514634L; public static final QYoutubeChannel youtubeChannel = new QYoutubeChannel("youtubeChannel"); diff --git a/src/main/generated/team7/inplace/global/exception/QErrorLog.java b/src/main/generated/team7/inplace/admin/error/QErrorLog.java similarity index 91% rename from src/main/generated/team7/inplace/global/exception/QErrorLog.java rename to src/main/generated/team7/inplace/admin/error/QErrorLog.java index ceffad2a..498fdfb0 100644 --- a/src/main/generated/team7/inplace/global/exception/QErrorLog.java +++ b/src/main/generated/team7/inplace/admin/error/QErrorLog.java @@ -1,4 +1,4 @@ -package team7.inplace.global.exception; +package team7.inplace.admin.error; import static com.querydsl.core.types.PathMetadataFactory.*; @@ -15,7 +15,7 @@ @Generated("com.querydsl.codegen.DefaultEntitySerializer") public class QErrorLog extends EntityPathBase { - private static final long serialVersionUID = 50351046L; + private static final long serialVersionUID = -245768895L; public static final QErrorLog errorLog = new QErrorLog("errorLog"); diff --git a/src/main/generated/team7/inplace/likedPlace/domain/QLikedPlace.java b/src/main/generated/team7/inplace/likedPlace/domain/QLikedPlace.java index 3657f5a9..092434c1 100644 --- a/src/main/generated/team7/inplace/likedPlace/domain/QLikedPlace.java +++ b/src/main/generated/team7/inplace/likedPlace/domain/QLikedPlace.java @@ -11,12 +11,12 @@ /** - * QLikedPlace is a Querydsl query type for likedPlace + * QLikedPlace is a Querydsl query type for LikedPlace */ @Generated("com.querydsl.codegen.DefaultEntitySerializer") public class QLikedPlace extends EntityPathBase { - private static final long serialVersionUID = 1786130640L; + private static final long serialVersionUID = -2070471504L; private static final PathInits INITS = PathInits.DIRECT2; @@ -48,11 +48,8 @@ public QLikedPlace(PathMetadata metadata, PathInits inits) { public QLikedPlace(Class type, PathMetadata metadata, PathInits inits) { super(type, metadata, inits); - this.place = inits.isInitialized("place") ? new team7.inplace.place.domain.QPlace( - forProperty("place"), inits.get("place")) : null; - this.user = - inits.isInitialized("user") ? new team7.inplace.user.domain.QUser(forProperty("user")) - : null; + this.place = inits.isInitialized("place") ? new team7.inplace.place.domain.QPlace(forProperty("place"), inits.get("place")) : null; + this.user = inits.isInitialized("user") ? new team7.inplace.user.domain.QUser(forProperty("user")) : null; } } diff --git a/src/main/java/team7/inplace/InplaceApplication.java b/src/main/java/team7/inplace/InplaceApplication.java index 887c1415..143ddd36 100644 --- a/src/main/java/team7/inplace/InplaceApplication.java +++ b/src/main/java/team7/inplace/InplaceApplication.java @@ -11,11 +11,11 @@ @SpringBootApplication @ConfigurationPropertiesScan @EnableJpaRepositories( - basePackages = "team7.inplace", - excludeFilters = @ComponentScan.Filter( - type = FilterType.ASSIGNABLE_TYPE, - classes = {RefreshTokenRepository.class} - ) + basePackages = "team7.inplace", + excludeFilters = @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = {RefreshTokenRepository.class} + ) ) public class InplaceApplication { diff --git a/src/main/java/team7/inplace/admin/AdminPageController.java b/src/main/java/team7/inplace/admin/AdminPageController.java index cfc69efa..f731c7e5 100644 --- a/src/main/java/team7/inplace/admin/AdminPageController.java +++ b/src/main/java/team7/inplace/admin/AdminPageController.java @@ -1,6 +1,5 @@ package team7.inplace.admin; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,12 +8,15 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import team7.inplace.global.exception.ErrorLog; -import team7.inplace.global.exception.ErrorLogRepository; +import team7.inplace.admin.banner.persistence.BannerRepository; +import team7.inplace.admin.error.ErrorLog; +import team7.inplace.admin.error.ErrorLogRepository; import team7.inplace.global.kakao.config.KakaoApiProperties; import team7.inplace.video.domain.Video; import team7.inplace.video.persistence.VideoRepository; +import java.util.List; + @Controller @RequiredArgsConstructor @RequestMapping("/admin") @@ -22,6 +24,7 @@ public class AdminPageController { private final KakaoApiProperties kakaoApiProperties; private final VideoRepository videoRepository; private final ErrorLogRepository errorLogRepository; + private final BannerRepository bannerRepository; @GetMapping("/video") public String getVideos(@PageableDefault Pageable pageable, Model model) { @@ -31,6 +34,7 @@ public String getVideos(@PageableDefault Pageable pageable, Model model) { model.addAttribute("totalPages", videoPage.getTotalPages()); model.addAttribute("isFirst", videoPage.isFirst()); model.addAttribute("isLast", videoPage.isLast()); + model.addAttribute("kakaoApiKey", kakaoApiProperties.jsKey()); return "admin/video.html"; } @@ -40,4 +44,17 @@ public String getErrorLogs(Model model) { model.addAttribute("errorLogs", errorLogs); return "admin/error-logs.html"; } + + @GetMapping("/banner") + public String getBanners(Model model) { + var banners = bannerRepository.findAll(); + + model.addAttribute("banners", banners); + return "admin/banner.html"; + } + + @GetMapping("/main") + public String getMainPage() { + return "admin/main.html"; + } } diff --git a/src/main/java/team7/inplace/admin/banner/application/BannerService.java b/src/main/java/team7/inplace/admin/banner/application/BannerService.java new file mode 100644 index 00000000..3faf4202 --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/application/BannerService.java @@ -0,0 +1,43 @@ +package team7.inplace.admin.banner.application; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import team7.inplace.admin.banner.application.command.BannerCommand.Create; +import team7.inplace.admin.banner.application.dto.BannerInfo; +import team7.inplace.admin.banner.application.dto.BannerInfo.Detail; +import team7.inplace.admin.banner.persistence.BannerRepository; +import team7.inplace.admin.banner.persistence.BannerS3Repository; +import team7.inplace.global.exception.InplaceException; +import team7.inplace.global.exception.code.BannerErrorCode; + +@Service +@RequiredArgsConstructor +public class BannerService { + private final BannerS3Repository bannerS3Repository; + private final BannerRepository bannerRepository; + + public void uploadBanner(Create command) { + var imgPath = bannerS3Repository.uploadBanner(command.imageFile()); + var banner = command.toEntity(imgPath); + bannerRepository.save(banner); + } + + public List getBanners() { + var now = LocalDateTime.now(); + var banners = bannerRepository.findActiveBanner(now); + + return banners.stream() + .sorted((a, b) -> Boolean.compare(b.getIsFixed(), a.getIsFixed())) + .map(BannerInfo.Detail::from) + .toList(); + } + + public void deleteBanner(Long id) { + var banner = bannerRepository.findById(id) + .orElseThrow(() -> InplaceException.of(BannerErrorCode.NOT_FOUND)); + bannerS3Repository.deleteBanner(banner.getImgPath()); + bannerRepository.delete(banner); + } +} diff --git a/src/main/java/team7/inplace/admin/banner/application/command/BannerCommand.java b/src/main/java/team7/inplace/admin/banner/application/command/BannerCommand.java new file mode 100644 index 00000000..ff4af7df --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/application/command/BannerCommand.java @@ -0,0 +1,19 @@ +package team7.inplace.admin.banner.application.command; + +import java.time.LocalDateTime; +import org.springframework.web.multipart.MultipartFile; +import team7.inplace.admin.banner.domain.Banner; + +public class BannerCommand { + public record Create( + String imgName, + MultipartFile imageFile, + LocalDateTime startDate, + LocalDateTime endDate, + Boolean isFixed + ) { + public Banner toEntity(String imgPath) { + return Banner.of(imgName, imgPath, startDate, endDate, isFixed); + } + } +} diff --git a/src/main/java/team7/inplace/admin/banner/application/dto/BannerInfo.java b/src/main/java/team7/inplace/admin/banner/application/dto/BannerInfo.java new file mode 100644 index 00000000..31b8900e --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/application/dto/BannerInfo.java @@ -0,0 +1,14 @@ +package team7.inplace.admin.banner.application.dto; + +import team7.inplace.admin.banner.domain.Banner; + +public class BannerInfo { + public record Detail( + Long id, + String imageUrl + ) { + public static Detail from(Banner banner) { + return new Detail(banner.getId(), banner.getImgPath()); + } + } +} diff --git a/src/main/java/team7/inplace/admin/banner/domain/Banner.java b/src/main/java/team7/inplace/admin/banner/domain/Banner.java new file mode 100644 index 00000000..f7a1edd7 --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/domain/Banner.java @@ -0,0 +1,38 @@ +package team7.inplace.admin.banner.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Banner { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String imgName; + private String imgPath; + private LocalDateTime startDate; + private LocalDateTime endDate; + private Boolean isFixed; + + private Banner(String imgName, String imgPath, LocalDateTime startDate, LocalDateTime endDate, Boolean isFixed) { + this.imgName = imgName; + this.imgPath = imgPath; + this.startDate = startDate; + this.endDate = endDate; + this.isFixed = isFixed; + } + + public static Banner of(String imgName, String imgPath, LocalDateTime startDate, LocalDateTime endDate, + Boolean isFixed) { + return new Banner(imgName, imgPath, startDate, endDate, isFixed); + } +} diff --git a/src/main/java/team7/inplace/admin/banner/persistence/BannerRepository.java b/src/main/java/team7/inplace/admin/banner/persistence/BannerRepository.java new file mode 100644 index 00000000..f8a21cc7 --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/persistence/BannerRepository.java @@ -0,0 +1,15 @@ +package team7.inplace.admin.banner.persistence; + +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import team7.inplace.admin.banner.domain.Banner; + +@Repository +public interface BannerRepository extends JpaRepository { + @Query("SELECT l FROM Banner l WHERE l.startDate <= :now AND l.endDate >= :now or l.isFixed = true") + List findActiveBanner(@Param("now") LocalDateTime now); +} \ No newline at end of file diff --git a/src/main/java/team7/inplace/admin/banner/persistence/BannerS3Repository.java b/src/main/java/team7/inplace/admin/banner/persistence/BannerS3Repository.java new file mode 100644 index 00000000..58bb87cd --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/persistence/BannerS3Repository.java @@ -0,0 +1,45 @@ +package team7.inplace.admin.banner.persistence; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; +import org.springframework.web.multipart.MultipartFile; +import team7.inplace.infra.s3.AwsProperties; + +@Repository +@Slf4j +@RequiredArgsConstructor +public class BannerS3Repository { + private final AmazonS3Client amazonS3Client; + private final AwsProperties awsProperties; + + public String uploadBanner(MultipartFile banner) { + var bucketName = awsProperties.bucketName(); + var key = "banner/" + UUID.randomUUID(); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(banner.getSize()); + metadata.setContentType(banner.getContentType()); + + try { + amazonS3Client.putObject(bucketName, key, banner.getInputStream(), metadata); + return "https://d4oqudml6s9ih.cloudfront.net/" + key; + } catch (Exception e) { + throw new RuntimeException("Failed to upload banner", e); + } + } + + public void deleteBanner(String imgPath) { + var bucketName = awsProperties.bucketName(); + var key = imgPath.substring(imgPath.lastIndexOf("banner")); + + try { + amazonS3Client.deleteObject(bucketName, key); + } catch (Exception e) { + throw new RuntimeException("Failed to delete banner", e); + } + } +} diff --git a/src/main/java/team7/inplace/admin/banner/presentation/BannerController.java b/src/main/java/team7/inplace/admin/banner/presentation/BannerController.java new file mode 100644 index 00000000..0ba731be --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/presentation/BannerController.java @@ -0,0 +1,43 @@ +package team7.inplace.admin.banner.presentation; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import team7.inplace.admin.banner.application.BannerService; + +@RestController +@Slf4j +@RequestMapping("/banners") +@RequiredArgsConstructor +public class BannerController { + private final BannerService bannerService; + + @PostMapping() + public void saveBanner(BannerRequest.Create request) { + + bannerService.uploadBanner(request.toCommand()); + } + + @GetMapping() + public ResponseEntity> getBanners() { + var banners = bannerService.getBanners(); + + var response = banners.stream() + .map(BannerResponse.Info::from) + .toList(); + return new ResponseEntity<>(response, HttpStatus.OK); + } + + @DeleteMapping("/{id}") + public void deleteBanner(@PathVariable Long id) { + bannerService.deleteBanner(id); + } +} diff --git a/src/main/java/team7/inplace/admin/banner/presentation/BannerRequest.java b/src/main/java/team7/inplace/admin/banner/presentation/BannerRequest.java new file mode 100644 index 00000000..8552327a --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/presentation/BannerRequest.java @@ -0,0 +1,25 @@ +package team7.inplace.admin.banner.presentation; + +import java.time.LocalDate; +import org.springframework.web.multipart.MultipartFile; +import team7.inplace.admin.banner.application.command.BannerCommand; + +public class BannerRequest { + public record Create( + String imageName, + MultipartFile imageFile, + LocalDate startDate, + LocalDate endDate, + Boolean isFixed + ) { + public BannerCommand.Create toCommand() { + return new BannerCommand.Create( + imageName, + imageFile, + startDate.atStartOfDay(), + endDate.atStartOfDay(), + isFixed + ); + } + } +} diff --git a/src/main/java/team7/inplace/admin/banner/presentation/BannerResponse.java b/src/main/java/team7/inplace/admin/banner/presentation/BannerResponse.java new file mode 100644 index 00000000..fdb32e8a --- /dev/null +++ b/src/main/java/team7/inplace/admin/banner/presentation/BannerResponse.java @@ -0,0 +1,14 @@ +package team7.inplace.admin.banner.presentation; + +import team7.inplace.admin.banner.application.dto.BannerInfo; + +public class BannerResponse { + public record Info( + Long id, + String imageUrl + ) { + public static Info from(BannerInfo.Detail banner) { + return new Info(banner.id(), banner.imageUrl()); + } + } +} diff --git a/src/main/java/team7/inplace/cicd/TestController.java b/src/main/java/team7/inplace/admin/cicd/TestController.java similarity index 90% rename from src/main/java/team7/inplace/cicd/TestController.java rename to src/main/java/team7/inplace/admin/cicd/TestController.java index 34bcc01a..9d07e49a 100644 --- a/src/main/java/team7/inplace/cicd/TestController.java +++ b/src/main/java/team7/inplace/admin/cicd/TestController.java @@ -1,4 +1,4 @@ -package team7.inplace.cicd; +package team7.inplace.admin.cicd; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/team7/inplace/crawling/application/AddressUtil.java b/src/main/java/team7/inplace/admin/crawling/application/AddressUtil.java similarity index 94% rename from src/main/java/team7/inplace/crawling/application/AddressUtil.java rename to src/main/java/team7/inplace/admin/crawling/application/AddressUtil.java index cfe07276..8bddfe09 100644 --- a/src/main/java/team7/inplace/crawling/application/AddressUtil.java +++ b/src/main/java/team7/inplace/admin/crawling/application/AddressUtil.java @@ -1,11 +1,12 @@ -package team7.inplace.crawling.application; - -import static lombok.AccessLevel.PRIVATE; +package team7.inplace.admin.crawling.application; import com.fasterxml.jackson.databind.JsonNode; +import lombok.NoArgsConstructor; + import java.util.regex.Matcher; import java.util.regex.Pattern; -import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PRIVATE; @NoArgsConstructor(access = PRIVATE) public final class AddressUtil { diff --git a/src/main/java/team7/inplace/crawling/application/CrawlingFacade.java b/src/main/java/team7/inplace/admin/crawling/application/CrawlingFacade.java similarity index 84% rename from src/main/java/team7/inplace/crawling/application/CrawlingFacade.java rename to src/main/java/team7/inplace/admin/crawling/application/CrawlingFacade.java index 771eb3ef..1a6f1e95 100644 --- a/src/main/java/team7/inplace/crawling/application/CrawlingFacade.java +++ b/src/main/java/team7/inplace/admin/crawling/application/CrawlingFacade.java @@ -1,8 +1,9 @@ -package team7.inplace.crawling.application; +package team7.inplace.admin.crawling.application; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import team7.inplace.crawling.application.dto.CrawlingInfo; +import org.springframework.scheduling.annotation.Scheduled; +import team7.inplace.admin.crawling.application.dto.CrawlingInfo; import team7.inplace.global.annotation.Facade; import team7.inplace.place.application.command.PlacesCommand; import team7.inplace.video.application.VideoFacade; @@ -16,7 +17,7 @@ public class CrawlingFacade { private final KakaoCrawlingService kakaoCrawlingService; private final VideoFacade videoFacade; - //TODO: 스케쥴링 추가 예정 + @Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul") public void updateVideos() { var crawlingInfos = youtubeCrawlingService.crawlAllVideos(); for (var crawlingInfo : crawlingInfos) { @@ -29,7 +30,7 @@ public void updateVideos() { } } - //TODO: 스케쥴링 추가 예정 + @Scheduled(cron = "0 30 2 * * *", zone = "Asia/Seoul") public void updateVideoView() { var crawlingInfos = videoCrawlingService.crawlingVideoView(); var videoCommands = crawlingInfos.stream() diff --git a/src/main/java/team7/inplace/crawling/application/KakaoCrawlingService.java b/src/main/java/team7/inplace/admin/crawling/application/KakaoCrawlingService.java similarity index 66% rename from src/main/java/team7/inplace/crawling/application/KakaoCrawlingService.java rename to src/main/java/team7/inplace/admin/crawling/application/KakaoCrawlingService.java index a3a8cb56..f9d3fd2a 100644 --- a/src/main/java/team7/inplace/crawling/application/KakaoCrawlingService.java +++ b/src/main/java/team7/inplace/admin/crawling/application/KakaoCrawlingService.java @@ -1,9 +1,9 @@ -package team7.inplace.crawling.application; +package team7.inplace.admin.crawling.application; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import team7.inplace.crawling.client.KakaoMapClient; -import team7.inplace.crawling.client.dto.PlaceNode; +import team7.inplace.admin.crawling.client.KakaoMapClient; +import team7.inplace.admin.crawling.client.dto.PlaceNode; @Service @RequiredArgsConstructor diff --git a/src/main/java/team7/inplace/crawling/application/VideoCrawlingService.java b/src/main/java/team7/inplace/admin/crawling/application/VideoCrawlingService.java similarity index 73% rename from src/main/java/team7/inplace/crawling/application/VideoCrawlingService.java rename to src/main/java/team7/inplace/admin/crawling/application/VideoCrawlingService.java index 45b0eb85..261df0a7 100644 --- a/src/main/java/team7/inplace/crawling/application/VideoCrawlingService.java +++ b/src/main/java/team7/inplace/admin/crawling/application/VideoCrawlingService.java @@ -1,13 +1,15 @@ -package team7.inplace.crawling.application; +package team7.inplace.admin.crawling.application; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import team7.inplace.crawling.application.dto.CrawlingInfo; -import team7.inplace.crawling.client.YoutubeClient; +import team7.inplace.admin.crawling.application.dto.CrawlingInfo; +import team7.inplace.admin.crawling.application.dto.CrawlingInfo.ViewInfo; +import team7.inplace.admin.crawling.client.YoutubeClient; import team7.inplace.video.persistence.VideoRepository; +import java.util.List; + @Service @RequiredArgsConstructor public class VideoCrawlingService { @@ -15,7 +17,7 @@ public class VideoCrawlingService { private final YoutubeClient youtubeClient; @Transactional(readOnly = true) - public List crawlingVideoView() { + public List crawlingVideoView() { var videos = videoRepository.findAll(); var videoInfos = videos.stream().map(video -> { diff --git a/src/main/java/team7/inplace/crawling/application/YoutubeCrawlingService.java b/src/main/java/team7/inplace/admin/crawling/application/YoutubeCrawlingService.java similarity index 89% rename from src/main/java/team7/inplace/crawling/application/YoutubeCrawlingService.java rename to src/main/java/team7/inplace/admin/crawling/application/YoutubeCrawlingService.java index 1e47107f..3d1322b7 100644 --- a/src/main/java/team7/inplace/crawling/application/YoutubeCrawlingService.java +++ b/src/main/java/team7/inplace/admin/crawling/application/YoutubeCrawlingService.java @@ -1,18 +1,19 @@ -package team7.inplace.crawling.application; +package team7.inplace.admin.crawling.application; -import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import team7.inplace.crawling.application.dto.CrawlingInfo; -import team7.inplace.crawling.client.KakaoMapClient; -import team7.inplace.crawling.client.YoutubeClient; -import team7.inplace.crawling.persistence.YoutubeChannelRepository; +import team7.inplace.admin.crawling.client.KakaoMapClient; +import team7.inplace.admin.crawling.client.YoutubeClient; +import team7.inplace.admin.crawling.persistence.YoutubeChannelRepository; +import team7.inplace.admin.crawling.application.dto.CrawlingInfo; import team7.inplace.global.exception.InplaceException; import team7.inplace.global.exception.code.ChannelErrorCode; +import java.util.List; +import java.util.Objects; + @Slf4j @Service @RequiredArgsConstructor diff --git a/src/main/java/team7/inplace/crawling/application/dto/CrawlingInfo.java b/src/main/java/team7/inplace/admin/crawling/application/dto/CrawlingInfo.java similarity index 93% rename from src/main/java/team7/inplace/crawling/application/dto/CrawlingInfo.java rename to src/main/java/team7/inplace/admin/crawling/application/dto/CrawlingInfo.java index 190a5a9b..82f72b53 100644 --- a/src/main/java/team7/inplace/crawling/application/dto/CrawlingInfo.java +++ b/src/main/java/team7/inplace/admin/crawling/application/dto/CrawlingInfo.java @@ -1,9 +1,9 @@ -package team7.inplace.crawling.application.dto; +package team7.inplace.admin.crawling.application.dto; import com.fasterxml.jackson.databind.JsonNode; import java.util.List; import java.util.Objects; -import team7.inplace.crawling.client.dto.PlaceNode; +import team7.inplace.admin.crawling.client.dto.PlaceNode; import team7.inplace.place.application.command.PlacesCommand; import team7.inplace.video.application.command.VideoCommand; diff --git a/src/main/java/team7/inplace/crawling/client/KakaoMapClient.java b/src/main/java/team7/inplace/admin/crawling/client/KakaoMapClient.java similarity index 96% rename from src/main/java/team7/inplace/crawling/client/KakaoMapClient.java rename to src/main/java/team7/inplace/admin/crawling/client/KakaoMapClient.java index e5e2284d..95d952e1 100644 --- a/src/main/java/team7/inplace/crawling/client/KakaoMapClient.java +++ b/src/main/java/team7/inplace/admin/crawling/client/KakaoMapClient.java @@ -1,7 +1,6 @@ -package team7.inplace.crawling.client; +package team7.inplace.admin.crawling.client; import com.fasterxml.jackson.databind.JsonNode; -import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; @@ -10,11 +9,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; -import team7.inplace.crawling.client.dto.PlaceNode; +import team7.inplace.admin.crawling.client.dto.PlaceNode; import team7.inplace.global.exception.InplaceException; import team7.inplace.global.exception.code.PlaceErrorCode; import team7.inplace.global.kakao.config.KakaoApiProperties; +import java.util.Objects; + @Slf4j @Component @RequiredArgsConstructor diff --git a/src/main/java/team7/inplace/crawling/client/YoutubeClient.java b/src/main/java/team7/inplace/admin/crawling/client/YoutubeClient.java similarity index 98% rename from src/main/java/team7/inplace/crawling/client/YoutubeClient.java rename to src/main/java/team7/inplace/admin/crawling/client/YoutubeClient.java index 71784077..152e4bb8 100644 --- a/src/main/java/team7/inplace/crawling/client/YoutubeClient.java +++ b/src/main/java/team7/inplace/admin/crawling/client/YoutubeClient.java @@ -1,14 +1,15 @@ -package team7.inplace.crawling.client; +package team7.inplace.admin.crawling.client; import com.fasterxml.jackson.databind.JsonNode; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + @Slf4j @Component public class YoutubeClient { diff --git a/src/main/java/team7/inplace/crawling/client/dto/PlaceNode.java b/src/main/java/team7/inplace/admin/crawling/client/dto/PlaceNode.java similarity index 84% rename from src/main/java/team7/inplace/crawling/client/dto/PlaceNode.java rename to src/main/java/team7/inplace/admin/crawling/client/dto/PlaceNode.java index 4f919cc3..397cb8dc 100644 --- a/src/main/java/team7/inplace/crawling/client/dto/PlaceNode.java +++ b/src/main/java/team7/inplace/admin/crawling/client/dto/PlaceNode.java @@ -1,4 +1,4 @@ -package team7.inplace.crawling.client.dto; +package team7.inplace.admin.crawling.client.dto; import com.fasterxml.jackson.databind.JsonNode; diff --git a/src/main/java/team7/inplace/crawling/domain/ChannelType.java b/src/main/java/team7/inplace/admin/crawling/domain/ChannelType.java similarity index 81% rename from src/main/java/team7/inplace/crawling/domain/ChannelType.java rename to src/main/java/team7/inplace/admin/crawling/domain/ChannelType.java index 1a4b47b1..5733cec5 100644 --- a/src/main/java/team7/inplace/crawling/domain/ChannelType.java +++ b/src/main/java/team7/inplace/admin/crawling/domain/ChannelType.java @@ -1,4 +1,4 @@ -package team7.inplace.crawling.domain; +package team7.inplace.admin.crawling.domain; public enum ChannelType { FOOD("FD6"); diff --git a/src/main/java/team7/inplace/crawling/domain/YoutubeChannel.java b/src/main/java/team7/inplace/admin/crawling/domain/YoutubeChannel.java similarity index 76% rename from src/main/java/team7/inplace/crawling/domain/YoutubeChannel.java rename to src/main/java/team7/inplace/admin/crawling/domain/YoutubeChannel.java index 1fadeab8..1b023d47 100644 --- a/src/main/java/team7/inplace/crawling/domain/YoutubeChannel.java +++ b/src/main/java/team7/inplace/admin/crawling/domain/YoutubeChannel.java @@ -1,16 +1,12 @@ -package team7.inplace.crawling.domain; +package team7.inplace.admin.crawling.domain; -import static jakarta.persistence.EnumType.STRING; - -import jakarta.persistence.Entity; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import static jakarta.persistence.EnumType.STRING; + @Getter @Entity(name = "youtube_channel") @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/team7/inplace/crawling/persistence/YoutubeChannelRepository.java b/src/main/java/team7/inplace/admin/crawling/persistence/YoutubeChannelRepository.java similarity index 70% rename from src/main/java/team7/inplace/crawling/persistence/YoutubeChannelRepository.java rename to src/main/java/team7/inplace/admin/crawling/persistence/YoutubeChannelRepository.java index 3c5146fa..b015a0cb 100644 --- a/src/main/java/team7/inplace/crawling/persistence/YoutubeChannelRepository.java +++ b/src/main/java/team7/inplace/admin/crawling/persistence/YoutubeChannelRepository.java @@ -1,8 +1,9 @@ -package team7.inplace.crawling.persistence; +package team7.inplace.admin.crawling.persistence; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import team7.inplace.crawling.domain.YoutubeChannel; +import team7.inplace.admin.crawling.domain.YoutubeChannel; + +import java.util.Optional; public interface YoutubeChannelRepository extends JpaRepository { Optional findYoutubeChannelByPlayListUUID(String playListUUID); diff --git a/src/main/java/team7/inplace/crawling/presentation/CrawlingController.java b/src/main/java/team7/inplace/admin/crawling/presentation/CrawlingController.java similarity index 61% rename from src/main/java/team7/inplace/crawling/presentation/CrawlingController.java rename to src/main/java/team7/inplace/admin/crawling/presentation/CrawlingController.java index 09c111bb..f616b992 100644 --- a/src/main/java/team7/inplace/crawling/presentation/CrawlingController.java +++ b/src/main/java/team7/inplace/admin/crawling/presentation/CrawlingController.java @@ -1,4 +1,4 @@ -package team7.inplace.crawling.presentation; +package team7.inplace.admin.crawling.presentation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import team7.inplace.crawling.application.CrawlingFacade; +import team7.inplace.admin.crawling.application.CrawlingFacade; @RestController @RequestMapping("/crawling") @@ -21,4 +21,18 @@ public ResponseEntity addPlaceInfo(@PathVariable Long videoId, @PathVariab return ResponseEntity.status(HttpStatus.CREATED).build(); } + + @PostMapping("/video") + public ResponseEntity crawlingVideo() { + crawlingFacade.updateVideos(); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @PostMapping("/video/view") + public ResponseEntity crawlingVideoView() { + crawlingFacade.updateVideoView(); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } } diff --git a/src/main/java/team7/inplace/global/exception/ErrorLog.java b/src/main/java/team7/inplace/admin/error/ErrorLog.java similarity index 79% rename from src/main/java/team7/inplace/global/exception/ErrorLog.java rename to src/main/java/team7/inplace/admin/error/ErrorLog.java index 4c909807..28cef11b 100644 --- a/src/main/java/team7/inplace/global/exception/ErrorLog.java +++ b/src/main/java/team7/inplace/admin/error/ErrorLog.java @@ -1,10 +1,6 @@ -package team7.inplace.global.exception; +package team7.inplace.admin.error; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/team7/inplace/admin/ErrorLogController.java b/src/main/java/team7/inplace/admin/error/ErrorLogController.java similarity index 89% rename from src/main/java/team7/inplace/admin/ErrorLogController.java rename to src/main/java/team7/inplace/admin/error/ErrorLogController.java index e73bdfb3..c093537f 100644 --- a/src/main/java/team7/inplace/admin/ErrorLogController.java +++ b/src/main/java/team7/inplace/admin/error/ErrorLogController.java @@ -1,4 +1,4 @@ -package team7.inplace.admin; +package team7.inplace.admin.error; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -6,7 +6,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import team7.inplace.global.exception.ErrorLogRepository; @RestController @RequiredArgsConstructor @@ -20,7 +19,7 @@ public ResponseEntity resolveErrorLog(@PathVariable Long id) { errorLog.resolve(); errorLogRepository.save(errorLog); }); - + return ResponseEntity.ok().build(); } } diff --git a/src/main/java/team7/inplace/global/exception/ErrorLogRepository.java b/src/main/java/team7/inplace/admin/error/ErrorLogRepository.java similarity index 83% rename from src/main/java/team7/inplace/global/exception/ErrorLogRepository.java rename to src/main/java/team7/inplace/admin/error/ErrorLogRepository.java index c25d0820..163ddff0 100644 --- a/src/main/java/team7/inplace/global/exception/ErrorLogRepository.java +++ b/src/main/java/team7/inplace/admin/error/ErrorLogRepository.java @@ -1,8 +1,9 @@ -package team7.inplace.global.exception; +package team7.inplace.admin.error; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ErrorLogRepository extends JpaRepository { List findByIsResolvedFalse(); } diff --git a/src/main/java/team7/inplace/favoriteInfluencer/application/FavoriteInfluencerService.java b/src/main/java/team7/inplace/favoriteInfluencer/application/FavoriteInfluencerService.java index 46ab48e1..0b37fb99 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/application/FavoriteInfluencerService.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/application/FavoriteInfluencerService.java @@ -1,7 +1,8 @@ package team7.inplace.favoriteInfluencer.application; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import team7.inplace.favoriteInfluencer.application.dto.FavoriteInfluencerCommand; @@ -10,12 +11,14 @@ import team7.inplace.favoriteInfluencer.persistent.FavoriteInfluencerRepository; import team7.inplace.global.exception.InplaceException; import team7.inplace.global.exception.code.AuthorizationErrorCode; -import team7.inplace.global.exception.code.UserErrorCode; +import team7.inplace.influencer.application.dto.InfluencerInfo; import team7.inplace.influencer.domain.Influencer; import team7.inplace.influencer.persistence.InfluencerRepository; +import team7.inplace.security.application.CurrentUserProvider; import team7.inplace.security.util.AuthorizationUtil; import team7.inplace.user.domain.User; -import team7.inplace.user.persistence.UserRepository; + +import java.util.List; @RequiredArgsConstructor @Service @@ -23,7 +26,7 @@ public class FavoriteInfluencerService { private final InfluencerRepository influencerRepository; private final FavoriteInfluencerRepository favoriteRepository; - private final UserRepository userRepository; + private final CurrentUserProvider currentUserProvider; @Transactional public void likeToInfluencer(FavoriteInfluencerCommand command) { @@ -31,17 +34,10 @@ public void likeToInfluencer(FavoriteInfluencerCommand command) { throw InplaceException.of(AuthorizationErrorCode.TOKEN_IS_EMPTY); } - Long userId = AuthorizationUtil.getUserId(); - User user = userRepository.findById(userId) - .orElseThrow(() -> InplaceException.of(UserErrorCode.NOT_FOUND)); + User user = currentUserProvider.getCurrentUser(); Influencer influencer = influencerRepository.findById(command.influencerId()).orElseThrow(); - FavoriteInfluencer favorite = favoriteRepository.findByUserIdAndInfluencerId(userId, - influencer.getId()) - .orElseGet(() -> new FavoriteInfluencer(user, influencer)); // 존재하지 않으면 새로 생성 - - favorite.updateLike(command.likes()); - favoriteRepository.save(favorite); + processFavoriteInfluencer(user, influencer, command.likes()); } @Transactional @@ -50,20 +46,32 @@ public void likeToManyInfluencer(FavoriteInfluencerListCommand command) { throw InplaceException.of(AuthorizationErrorCode.TOKEN_IS_EMPTY); } - Long userId = AuthorizationUtil.getUserId(); - User user = userRepository.findById(userId) - .orElseThrow(() -> InplaceException.of(UserErrorCode.NOT_FOUND)); - + User user = currentUserProvider.getCurrentUser(); List influencers = influencerRepository.findAllById(command.influencerIds()); for (Influencer influencer : influencers) { + processFavoriteInfluencer(user, influencer, command.likes()); + } + } - FavoriteInfluencer favorite = favoriteRepository.findByUserIdAndInfluencerId(userId, - influencer.getId()) + private void processFavoriteInfluencer(User user, Influencer influencer, Boolean likes) { + FavoriteInfluencer favorite = favoriteRepository + .findByUserIdAndInfluencerId(user.getId(), influencer.getId()) .orElseGet(() -> new FavoriteInfluencer(user, influencer)); // 존재하지 않으면 새로 생성 - favorite.updateLike(command.likes()); + favorite.updateLike(likes); + if (favorite.getId() == null) { favoriteRepository.save(favorite); } } + + @Transactional(readOnly = true) + public Page getFavoriteInfluencers(Long userId, Pageable pageable) { + Page influencerPage = favoriteRepository.findByUserIdAndIsLikedTrue( + userId, pageable); + + return influencerPage.map( + favorite -> InfluencerInfo.from(favorite.getInfluencer(), favorite.isLiked())); + } } + diff --git a/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerCommand.java b/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerCommand.java index e9f4bc8f..f2f318c0 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerCommand.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerCommand.java @@ -1,8 +1,8 @@ package team7.inplace.favoriteInfluencer.application.dto; public record FavoriteInfluencerCommand( - Long influencerId, - Boolean likes + Long influencerId, + Boolean likes ) { } diff --git a/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerListCommand.java b/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerListCommand.java index ebf7dd67..3a167412 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerListCommand.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/application/dto/FavoriteInfluencerListCommand.java @@ -3,8 +3,8 @@ import java.util.List; public record FavoriteInfluencerListCommand( - List influencerIds, - Boolean likes + List influencerIds, + Boolean likes ) { } diff --git a/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java b/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java index 5d0fa4f2..0b8612f9 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/domain/FavoriteInfluencer.java @@ -1,14 +1,6 @@ package team7.inplace.favoriteInfluencer.domain; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; @@ -16,6 +8,9 @@ import team7.inplace.influencer.domain.Influencer; import team7.inplace.user.domain.User; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + @Getter @RequiredArgsConstructor @NoArgsConstructor(access = PROTECTED) diff --git a/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java b/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java index e850f167..ead22138 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/persistent/FavoriteInfluencerRepository.java @@ -1,17 +1,22 @@ package team7.inplace.favoriteInfluencer.persistent; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import team7.inplace.favoriteInfluencer.domain.FavoriteInfluencer; +import java.util.List; +import java.util.Optional; +import java.util.Set; + public interface FavoriteInfluencerRepository extends JpaRepository { List findByUserId(Long userId); + Page findByUserIdAndIsLikedTrue(Long userId, Pageable pageable); + Optional findByUserIdAndInfluencerId(Long userId, Long influencerId); @Query("SELECT f.influencer.id FROM FavoriteInfluencer f WHERE f.user.id = :userId AND f.isLiked = true") diff --git a/src/main/java/team7/inplace/favoriteInfluencer/presentation/FavoriteInfluencerController.java b/src/main/java/team7/inplace/favoriteInfluencer/presentation/FavoriteInfluencerController.java index f60c8576..72baebb8 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/presentation/FavoriteInfluencerController.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/presentation/FavoriteInfluencerController.java @@ -29,7 +29,7 @@ public ResponseEntity likeToInfluencer(@RequestBody InfluencerLikeRequest @PostMapping("/multiple/likes") public ResponseEntity likeToManyInfluencer( - @RequestBody InfluencerListLikeRequest request) { + @RequestBody InfluencerListLikeRequest request) { FavoriteInfluencerListCommand command = request.toCommand(); favoriteInfluencerService.likeToManyInfluencer(command); return new ResponseEntity<>(HttpStatus.OK); diff --git a/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerLikeRequest.java b/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerLikeRequest.java index 0ee70a78..5c2f9fc1 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerLikeRequest.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerLikeRequest.java @@ -3,8 +3,8 @@ import team7.inplace.favoriteInfluencer.application.dto.FavoriteInfluencerCommand; public record InfluencerLikeRequest( - Long influencerId, - Boolean likes + Long influencerId, + Boolean likes ) { public FavoriteInfluencerCommand toCommand() { diff --git a/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerListLikeRequest.java b/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerListLikeRequest.java index 6f5fa80a..ec4406f2 100644 --- a/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerListLikeRequest.java +++ b/src/main/java/team7/inplace/favoriteInfluencer/presentation/dto/InfluencerListLikeRequest.java @@ -1,11 +1,12 @@ package team7.inplace.favoriteInfluencer.presentation.dto; -import java.util.List; import team7.inplace.favoriteInfluencer.application.dto.FavoriteInfluencerListCommand; +import java.util.List; + public record InfluencerListLikeRequest( - List influencerIds, - Boolean likes + List influencerIds, + Boolean likes ) { public FavoriteInfluencerListCommand toCommand() { diff --git a/src/main/java/team7/inplace/global/annotation/Facade.java b/src/main/java/team7/inplace/global/annotation/Facade.java index 1b0f2071..70fe10e2 100644 --- a/src/main/java/team7/inplace/global/annotation/Facade.java +++ b/src/main/java/team7/inplace/global/annotation/Facade.java @@ -1,13 +1,10 @@ package team7.inplace.global.annotation; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; +import java.lang.annotation.*; + @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/src/main/java/team7/inplace/global/exception/InplaceExceptionHandler.java b/src/main/java/team7/inplace/global/exception/InplaceExceptionHandler.java index dd7426a6..49219fea 100644 --- a/src/main/java/team7/inplace/global/exception/InplaceExceptionHandler.java +++ b/src/main/java/team7/inplace/global/exception/InplaceExceptionHandler.java @@ -1,7 +1,6 @@ package team7.inplace.global.exception; import jakarta.servlet.http.HttpServletRequest; -import java.net.URI; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -9,6 +8,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import team7.inplace.admin.error.ErrorLog; +import team7.inplace.admin.error.ErrorLogRepository; + +import java.net.URI; @Slf4j @RestControllerAdvice diff --git a/src/main/java/team7/inplace/global/exception/code/AuthorizationErrorCode.java b/src/main/java/team7/inplace/global/exception/code/AuthorizationErrorCode.java index 640aaccf..f4502a93 100644 --- a/src/main/java/team7/inplace/global/exception/code/AuthorizationErrorCode.java +++ b/src/main/java/team7/inplace/global/exception/code/AuthorizationErrorCode.java @@ -7,9 +7,10 @@ @AllArgsConstructor @Getter public enum AuthorizationErrorCode implements ErrorCode { - TOKEN_IS_EMPTY(HttpStatus.BAD_REQUEST, "A001", "Token is Empty"), - INVALID_TOKEN(HttpStatus.BAD_REQUEST, "A002", "Invalid Token"), - TOKEN_IS_EXPIRED(HttpStatus.BAD_REQUEST, "A003", "Token is Expired"); + NOT_AUTHENTICATION(HttpStatus.UNAUTHORIZED, "A000", "Authentication failed"), + TOKEN_IS_EMPTY(HttpStatus.BAD_REQUEST, "A001", "not include token"), + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "A002", "Invalid token"), + TOKEN_IS_EXPIRED(HttpStatus.BAD_REQUEST, "A003", "token is expired"); private final HttpStatus httpStatus; private final String errorCode; diff --git a/src/main/java/team7/inplace/global/exception/code/BannerErrorCode.java b/src/main/java/team7/inplace/global/exception/code/BannerErrorCode.java new file mode 100644 index 00000000..20809c4c --- /dev/null +++ b/src/main/java/team7/inplace/global/exception/code/BannerErrorCode.java @@ -0,0 +1,32 @@ +package team7.inplace.global.exception.code; + +import org.springframework.http.HttpStatus; + +public enum BannerErrorCode implements ErrorCode { + NOT_FOUND("B001", "배너를 찾을 수 없습니다.", HttpStatus.NOT_FOUND); + + private final String code; + private final String message; + private final HttpStatus httpStatus; + + BannerErrorCode(String code, String message, HttpStatus httpStatus) { + this.code = code; + this.message = message; + this.httpStatus = httpStatus; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String code() { + return code; + } + + @Override + public String message() { + return message; + } +} diff --git a/src/main/java/team7/inplace/global/exception/code/VideoErrorCode.java b/src/main/java/team7/inplace/global/exception/code/VideoErrorCode.java index a1c6ea37..855ff964 100644 --- a/src/main/java/team7/inplace/global/exception/code/VideoErrorCode.java +++ b/src/main/java/team7/inplace/global/exception/code/VideoErrorCode.java @@ -6,7 +6,7 @@ @AllArgsConstructor @Getter -public enum VideoErrorCode implements ErrorCode{ +public enum VideoErrorCode implements ErrorCode { NOT_FOUND(HttpStatus.NOT_FOUND, "V001", "Can't find such video info"); private final HttpStatus httpStatus; diff --git a/src/main/java/team7/inplace/global/kakao/config/KakaoApiProperties.java b/src/main/java/team7/inplace/global/kakao/config/KakaoApiProperties.java index c725c472..32285510 100644 --- a/src/main/java/team7/inplace/global/kakao/config/KakaoApiProperties.java +++ b/src/main/java/team7/inplace/global/kakao/config/KakaoApiProperties.java @@ -4,8 +4,9 @@ @ConfigurationProperties(prefix = "kakao.api") public record KakaoApiProperties( - String key, - String sendMessageToMeUrl + String key, + String jsKey, + String sendMessageToMeUrl ) { public String getAuthorization() { diff --git a/src/main/java/team7/inplace/global/rest/WebClientConfig.java b/src/main/java/team7/inplace/global/rest/WebClientConfig.java index 404309ba..b968aaa5 100644 --- a/src/main/java/team7/inplace/global/rest/WebClientConfig.java +++ b/src/main/java/team7/inplace/global/rest/WebClientConfig.java @@ -2,7 +2,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; @Configuration public class WebClientConfig { @@ -10,7 +14,10 @@ public class WebClientConfig { @Bean public WebClient webClient() { return WebClient.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) - .build(); + .clientConnector(new ReactorClientHttpConnector( + HttpClient.create().responseTimeout(Duration.ofMillis(60000)) + )) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) + .build(); } } diff --git a/src/main/java/team7/inplace/global/scheduler/ScheduledExecutorConfig.java b/src/main/java/team7/inplace/global/scheduler/ScheduledExecutorConfig.java index afcc36c0..0c4f502e 100644 --- a/src/main/java/team7/inplace/global/scheduler/ScheduledExecutorConfig.java +++ b/src/main/java/team7/inplace/global/scheduler/ScheduledExecutorConfig.java @@ -4,8 +4,10 @@ import java.util.concurrent.ScheduledExecutorService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; @Configuration +@EnableScheduling public class ScheduledExecutorConfig { @Bean diff --git a/src/main/java/team7/inplace/influencer/application/InfluencerService.java b/src/main/java/team7/inplace/influencer/application/InfluencerService.java index e8ec9cf9..c8a03cb4 100644 --- a/src/main/java/team7/inplace/influencer/application/InfluencerService.java +++ b/src/main/java/team7/inplace/influencer/application/InfluencerService.java @@ -1,7 +1,5 @@ package team7.inplace.influencer.application; -import java.util.List; -import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -17,6 +15,9 @@ import team7.inplace.security.util.AuthorizationUtil; import team7.inplace.user.persistence.UserRepository; +import java.util.List; +import java.util.Set; + @RequiredArgsConstructor @Service public class InfluencerService { @@ -39,12 +40,12 @@ public Page getAllInfluencers(Pageable pageable) { Set likedInfluencerIds = favoriteRepository.findLikedInfluencerIdsByUserId(userId); List influencerInfos = influencersPage.stream() - .map(influencer -> { - boolean isLiked = likedInfluencerIds.contains(influencer.getId()); - return InfluencerInfo.from(influencer, isLiked); - }) - .sorted((a, b) -> Boolean.compare(b.likes(), a.likes())) - .toList(); + .map(influencer -> { + boolean isLiked = likedInfluencerIds.contains(influencer.getId()); + return InfluencerInfo.from(influencer, isLiked); + }) + .sorted((a, b) -> Boolean.compare(b.likes(), a.likes())) + .toList(); return new PageImpl<>(influencerInfos, pageable, influencersPage.getTotalElements()); } @@ -53,8 +54,8 @@ public Page getAllInfluencers(Pageable pageable) { public List getAllInfluencerNames() { List names = influencerRepository.findAllInfluencerNames(); return names.stream() - .map(InfluencerNameInfo::new) - .toList(); + .map(InfluencerNameInfo::new) + .toList(); } @Transactional @@ -67,7 +68,7 @@ public Long createInfluencer(InfluencerCommand command) { public Long updateInfluencer(Long id, InfluencerCommand command) { Influencer influencer = influencerRepository.findById(id).orElseThrow(); influencer.update(command.influencerName(), command.influencerImgUrl(), - command.influencerJob()); + command.influencerJob()); return influencer.getId(); } diff --git a/src/main/java/team7/inplace/influencer/application/dto/InfluencerCommand.java b/src/main/java/team7/inplace/influencer/application/dto/InfluencerCommand.java index c0ac6e03..99e0e9bd 100644 --- a/src/main/java/team7/inplace/influencer/application/dto/InfluencerCommand.java +++ b/src/main/java/team7/inplace/influencer/application/dto/InfluencerCommand.java @@ -3,16 +3,16 @@ import team7.inplace.influencer.domain.Influencer; public record InfluencerCommand( - String influencerName, - String influencerImgUrl, - String influencerJob + String influencerName, + String influencerImgUrl, + String influencerJob ) { public static Influencer to(InfluencerCommand influencerCommand) { return new Influencer( - influencerCommand.influencerName, - influencerCommand.influencerImgUrl, - influencerCommand.influencerJob + influencerCommand.influencerName, + influencerCommand.influencerImgUrl, + influencerCommand.influencerJob ); } } diff --git a/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java b/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java index ca82957f..7f65194a 100644 --- a/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java +++ b/src/main/java/team7/inplace/influencer/application/dto/InfluencerInfo.java @@ -3,20 +3,20 @@ import team7.inplace.influencer.domain.Influencer; public record InfluencerInfo( - Long influencerId, - String influencerName, - String influencerImgUrl, - String influencerJob, - boolean likes + Long influencerId, + String influencerName, + String influencerImgUrl, + String influencerJob, + boolean likes ) { public static InfluencerInfo from(Influencer influencer, boolean isLiked) { return new InfluencerInfo( - influencer.getId(), - influencer.getName(), - influencer.getImgUrl(), - influencer.getJob(), - isLiked + influencer.getId(), + influencer.getName(), + influencer.getImgUrl(), + influencer.getJob(), + isLiked ); } } diff --git a/src/main/java/team7/inplace/influencer/domain/Influencer.java b/src/main/java/team7/inplace/influencer/domain/Influencer.java index e35d1899..69154ac6 100644 --- a/src/main/java/team7/inplace/influencer/domain/Influencer.java +++ b/src/main/java/team7/inplace/influencer/domain/Influencer.java @@ -1,16 +1,12 @@ package team7.inplace.influencer.domain; -import static lombok.AccessLevel.PROTECTED; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import static lombok.AccessLevel.PROTECTED; + @Getter @NoArgsConstructor(access = PROTECTED) @AllArgsConstructor diff --git a/src/main/java/team7/inplace/influencer/persistence/InfluencerRepository.java b/src/main/java/team7/inplace/influencer/persistence/InfluencerRepository.java index 8a620738..40b4e5f5 100644 --- a/src/main/java/team7/inplace/influencer/persistence/InfluencerRepository.java +++ b/src/main/java/team7/inplace/influencer/persistence/InfluencerRepository.java @@ -1,12 +1,13 @@ package team7.inplace.influencer.persistence; -import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import team7.inplace.influencer.domain.Influencer; +import java.util.List; + public interface InfluencerRepository extends JpaRepository { @Override diff --git a/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java b/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java index 23cacb42..f9738682 100644 --- a/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java +++ b/src/main/java/team7/inplace/influencer/presentation/InfluencerController.java @@ -1,26 +1,20 @@ package team7.inplace.influencer.presentation; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import team7.inplace.influencer.application.InfluencerService; import team7.inplace.influencer.application.dto.InfluencerCommand; import team7.inplace.influencer.presentation.dto.InfluencerNameResponse; import team7.inplace.influencer.presentation.dto.InfluencerRequest; import team7.inplace.influencer.presentation.dto.InfluencerResponse; +import java.util.List; + @RequiredArgsConstructor @RestController @RequestMapping("/influencers") @@ -30,9 +24,9 @@ public class InfluencerController implements InfluencerControllerApiSpec { @GetMapping() public ResponseEntity> getAllInfluencers( - @PageableDefault(page = 0, size = 10) Pageable pageable) { + @PageableDefault(page = 0, size = 10) Pageable pageable) { Page influencers = influencerService.getAllInfluencers(pageable) - .map(InfluencerResponse::from); + .map(InfluencerResponse::from); return new ResponseEntity<>(influencers, HttpStatus.OK); } @@ -40,8 +34,8 @@ public ResponseEntity> getAllInfluencers( @GetMapping("/names") public ResponseEntity> getAllInfluencerNames() { List names = influencerService.getAllInfluencerNames().stream() - .map(InfluencerNameResponse::from) - .toList(); + .map(InfluencerNameResponse::from) + .toList(); return new ResponseEntity<>(names, HttpStatus.OK); } @@ -55,13 +49,13 @@ public ResponseEntity createInfluencer(@RequestBody InfluencerRequest requ @PutMapping("/{id}") public ResponseEntity updateInfluencer( - @PathVariable Long id, - @RequestBody InfluencerRequest request + @PathVariable Long id, + @RequestBody InfluencerRequest request ) { InfluencerCommand influencerCommand = new InfluencerCommand( - request.influencerName(), - request.influencerImgUrl(), - request.influencerJob() + request.influencerName(), + request.influencerImgUrl(), + request.influencerJob() ); Long updatedId = influencerService.updateInfluencer(id, influencerCommand); diff --git a/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java b/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java index 520853b7..dccc9b7c 100644 --- a/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java +++ b/src/main/java/team7/inplace/influencer/presentation/InfluencerControllerApiSpec.java @@ -1,7 +1,6 @@ package team7.inplace.influencer.presentation; import io.swagger.v3.oas.annotations.Operation; -import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -11,6 +10,8 @@ import team7.inplace.influencer.presentation.dto.InfluencerRequest; import team7.inplace.influencer.presentation.dto.InfluencerResponse; +import java.util.List; + public interface InfluencerControllerApiSpec { @Operation(summary = "인플루언서들 반환", description = "토큰이 있는 경우 좋아요된 인플루언서가 먼저 반환됩니다.") @@ -24,7 +25,7 @@ public interface InfluencerControllerApiSpec { @Operation(summary = "인플루언서 수정", description = "인플루언서를 수정합니다.") ResponseEntity updateInfluencer(@PathVariable Long id, - @RequestBody InfluencerRequest request); + @RequestBody InfluencerRequest request); @Operation(summary = "인플루언서 삭제", description = "인플루언서를 삭제합니다.") ResponseEntity deleteInfluencer(@PathVariable Long id); diff --git a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerNameResponse.java b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerNameResponse.java index e5c06660..fc821427 100644 --- a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerNameResponse.java +++ b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerNameResponse.java @@ -3,12 +3,12 @@ import team7.inplace.influencer.application.dto.InfluencerNameInfo; public record InfluencerNameResponse( - String influencerName + String influencerName ) { public static InfluencerNameResponse from(InfluencerNameInfo influencerNameInfo) { return new InfluencerNameResponse( - influencerNameInfo.name() + influencerNameInfo.name() ); } } diff --git a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java index a5ca7a26..2ac73abe 100644 --- a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java +++ b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerRequest.java @@ -3,16 +3,16 @@ import team7.inplace.influencer.application.dto.InfluencerCommand; public record InfluencerRequest( - String influencerName, - String influencerImgUrl, - String influencerJob + String influencerName, + String influencerImgUrl, + String influencerJob ) { public static InfluencerCommand to(InfluencerRequest request) { return new InfluencerCommand( - request.influencerName(), - request.influencerImgUrl(), - request.influencerJob() + request.influencerName(), + request.influencerImgUrl(), + request.influencerJob() ); } } diff --git a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerResponse.java b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerResponse.java index 12729aad..a2eb53a2 100644 --- a/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerResponse.java +++ b/src/main/java/team7/inplace/influencer/presentation/dto/InfluencerResponse.java @@ -3,20 +3,20 @@ import team7.inplace.influencer.application.dto.InfluencerInfo; public record InfluencerResponse( - Long influencerId, - String influencerName, - String influencerImgUrl, - String influencerJob, - boolean likes + Long influencerId, + String influencerName, + String influencerImgUrl, + String influencerJob, + boolean likes ) { public static InfluencerResponse from(InfluencerInfo influencerInfo) { return new InfluencerResponse( - influencerInfo.influencerId(), - influencerInfo.influencerName(), - influencerInfo.influencerImgUrl(), - influencerInfo.influencerJob(), - influencerInfo.likes() + influencerInfo.influencerId(), + influencerInfo.influencerName(), + influencerInfo.influencerImgUrl(), + influencerInfo.influencerJob(), + influencerInfo.likes() ); } } diff --git a/src/main/java/team7/inplace/infra/s3/AwsProperties.java b/src/main/java/team7/inplace/infra/s3/AwsProperties.java new file mode 100644 index 00000000..4b470cad --- /dev/null +++ b/src/main/java/team7/inplace/infra/s3/AwsProperties.java @@ -0,0 +1,12 @@ +package team7.inplace.infra.s3; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "aws") +public record AwsProperties( + String accessKey, + String secretKey, + String region, + String bucketName +) { +} diff --git a/src/main/java/team7/inplace/infra/s3/S3Config.java b/src/main/java/team7/inplace/infra/s3/S3Config.java new file mode 100644 index 00000000..89ed79b3 --- /dev/null +++ b/src/main/java/team7/inplace/infra/s3/S3Config.java @@ -0,0 +1,26 @@ +package team7.inplace.infra.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class S3Config { + private final AwsProperties awsProperties; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(awsProperties.accessKey(), + awsProperties.secretKey()); + + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(awsProperties.region()) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/team7/inplace/likedPlace/domain/LikedPlace.java b/src/main/java/team7/inplace/likedPlace/domain/LikedPlace.java index 35cdab35..ed31817c 100644 --- a/src/main/java/team7/inplace/likedPlace/domain/LikedPlace.java +++ b/src/main/java/team7/inplace/likedPlace/domain/LikedPlace.java @@ -1,15 +1,6 @@ package team7.inplace.likedPlace.domain; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; @@ -17,6 +8,9 @@ import team7.inplace.place.domain.Place; import team7.inplace.user.domain.User; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + @Getter @RequiredArgsConstructor @NoArgsConstructor(access = PROTECTED) diff --git a/src/main/java/team7/inplace/likedPlace/persistence/LikedPlaceRepository.java b/src/main/java/team7/inplace/likedPlace/persistence/LikedPlaceRepository.java index ed63160b..0ae36e7f 100644 --- a/src/main/java/team7/inplace/likedPlace/persistence/LikedPlaceRepository.java +++ b/src/main/java/team7/inplace/likedPlace/persistence/LikedPlaceRepository.java @@ -1,10 +1,23 @@ package team7.inplace.likedPlace.persistence; import java.util.Optional; +import java.util.Set; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import team7.inplace.likedPlace.domain.LikedPlace; public interface LikedPlaceRepository extends JpaRepository { Optional findByUserIdAndPlaceId(Long userId, Long placeId); + + @Query("SELECT l.place.id FROM LikedPlace l WHERE l.user.id = :userId AND l.isLiked = true") + Set findPlaceIdsByUserIdAndIsLikedTrue(@Param("userId") Long userId); + + @Query("SELECT lp FROM LikedPlace lp JOIN FETCH lp.place WHERE lp.user.id = :userId AND lp.isLiked = true") + Page findByUserIdAndIsLikedTrueWithPlace(@Param("userId") Long userId, + Pageable pageable); + } diff --git a/src/main/java/team7/inplace/oauthToken/application/OauthTokenService.java b/src/main/java/team7/inplace/oauthToken/application/OauthTokenService.java index aec4df0b..5a031c4f 100644 --- a/src/main/java/team7/inplace/oauthToken/application/OauthTokenService.java +++ b/src/main/java/team7/inplace/oauthToken/application/OauthTokenService.java @@ -23,17 +23,17 @@ public class OauthTokenService { @Transactional(readOnly = true) public String findOAuthTokenByUserId(Long userId) throws InplaceException { return tokenEncryptionUtil.decrypt(oauthTokenRepository.findByUserId(userId) - .orElseThrow(() -> InplaceException.of(UserErrorCode.OAUTH_TOKEN_NOT_FOUND)) - .getOauthToken()); + .orElseThrow(() -> InplaceException.of(UserErrorCode.OAUTH_TOKEN_NOT_FOUND)) + .getOauthToken()); } @Transactional public void insertOauthToken(OauthTokenCommand oauthTokenCommand) throws InplaceException { User userProxy = entityManager.getReference(User.class, oauthTokenCommand.userId()); OauthToken oauthToken = OauthToken.of( - tokenEncryptionUtil.encrypt(oauthTokenCommand.oauthToken()), - oauthTokenCommand.expiresAt(), - userProxy + tokenEncryptionUtil.encrypt(oauthTokenCommand.oauthToken()), + oauthTokenCommand.expiresAt(), + userProxy ); oauthTokenRepository.save(oauthToken); @@ -42,11 +42,11 @@ public void insertOauthToken(OauthTokenCommand oauthTokenCommand) throws Inplace @Transactional public void updateOauthToken(OauthTokenCommand oauthTokenCommand) throws InplaceException { OauthToken oauthToken = oauthTokenRepository.findByUserId(oauthTokenCommand.userId()) - .orElseThrow(() -> InplaceException.of(UserErrorCode.OAUTH_TOKEN_NOT_FOUND)); + .orElseThrow(() -> InplaceException.of(UserErrorCode.OAUTH_TOKEN_NOT_FOUND)); oauthToken.updateInfo( - tokenEncryptionUtil.encrypt(oauthTokenCommand.oauthToken()), - oauthTokenCommand.expiresAt() + tokenEncryptionUtil.encrypt(oauthTokenCommand.oauthToken()), + oauthTokenCommand.expiresAt() ); } diff --git a/src/main/java/team7/inplace/oauthToken/application/command/OauthTokenCommand.java b/src/main/java/team7/inplace/oauthToken/application/command/OauthTokenCommand.java index c8b53f54..5b1aa3e9 100644 --- a/src/main/java/team7/inplace/oauthToken/application/command/OauthTokenCommand.java +++ b/src/main/java/team7/inplace/oauthToken/application/command/OauthTokenCommand.java @@ -3,9 +3,9 @@ import java.time.Instant; public record OauthTokenCommand( - String oauthToken, - Instant expiresAt, - Long userId + String oauthToken, + Instant expiresAt, + Long userId ) { public static OauthTokenCommand of(String oauthToken, Instant expiresAt, Long userId) { diff --git a/src/main/java/team7/inplace/oauthToken/domain/OauthToken.java b/src/main/java/team7/inplace/oauthToken/domain/OauthToken.java index cc9c77a5..8e4eadfb 100644 --- a/src/main/java/team7/inplace/oauthToken/domain/OauthToken.java +++ b/src/main/java/team7/inplace/oauthToken/domain/OauthToken.java @@ -1,20 +1,15 @@ package team7.inplace.oauthToken.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import team7.inplace.user.domain.User; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/team7/inplace/oauthToken/persistence/OauthTokenRepository.java b/src/main/java/team7/inplace/oauthToken/persistence/OauthTokenRepository.java index 4bb2c499..bf09ccd5 100644 --- a/src/main/java/team7/inplace/oauthToken/persistence/OauthTokenRepository.java +++ b/src/main/java/team7/inplace/oauthToken/persistence/OauthTokenRepository.java @@ -1,10 +1,11 @@ package team7.inplace.oauthToken.persistence; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import team7.inplace.oauthToken.domain.OauthToken; +import java.util.Optional; + @Repository public interface OauthTokenRepository extends JpaRepository { diff --git a/src/main/java/team7/inplace/place/application/CategoryService.java b/src/main/java/team7/inplace/place/application/CategoryService.java index e3103751..f8ad2ff6 100644 --- a/src/main/java/team7/inplace/place/application/CategoryService.java +++ b/src/main/java/team7/inplace/place/application/CategoryService.java @@ -1,13 +1,14 @@ package team7.inplace.place.application; -import java.util.Arrays; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import team7.inplace.place.application.dto.CategoryInfo; import team7.inplace.place.domain.Category; import team7.inplace.place.persistence.PlaceRepository; +import java.util.Arrays; +import java.util.List; + @Service @RequiredArgsConstructor public class CategoryService { @@ -16,6 +17,6 @@ public class CategoryService { public List getCategories() { return Arrays.stream(Category.values()).map(category -> new CategoryInfo(category.name())) - .toList(); + .toList(); } } diff --git a/src/main/java/team7/inplace/place/application/PlaceService.java b/src/main/java/team7/inplace/place/application/PlaceService.java index a1b1144f..0fb0161f 100644 --- a/src/main/java/team7/inplace/place/application/PlaceService.java +++ b/src/main/java/team7/inplace/place/application/PlaceService.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import team7.inplace.global.exception.InplaceException; import team7.inplace.global.exception.code.AuthorizationErrorCode; @@ -21,11 +22,14 @@ import team7.inplace.place.application.command.PlacesCommand.Create; import team7.inplace.place.application.command.PlacesCommand.PlacesCoordinateCommand; import team7.inplace.place.application.command.PlacesCommand.PlacesFilterParamsCommand; +import team7.inplace.place.application.dto.LikedPlaceInfo; import team7.inplace.place.application.dto.PlaceDetailInfo; import team7.inplace.place.application.dto.PlaceInfo; +import team7.inplace.place.domain.Category; import team7.inplace.place.domain.Place; import team7.inplace.place.persistence.PlaceRepository; import team7.inplace.placeMessage.application.command.PlaceMessageCommand; +import team7.inplace.review.persistence.ReviewRepository; import team7.inplace.security.util.AuthorizationUtil; import team7.inplace.user.domain.User; import team7.inplace.user.persistence.UserRepository; @@ -44,13 +48,18 @@ public class PlaceService { private final LikedPlaceRepository likedPlaceRepository; + private final ReviewRepository reviewRepository; + public Page getPlacesWithinRadius( PlacesCoordinateCommand placesCoordinateCommand, PlacesFilterParamsCommand placesFilterParamsCommand) { // categories와 influencers 필터 처리 List categoryFilters = placesFilterParamsCommand.isCategoryFilterExists() - ? Arrays.stream(placesFilterParamsCommand.categories().split(",")).toList() + ? Arrays.stream(placesFilterParamsCommand.categories().split(",")) + .map(Category::of) + .map(Category::name) + .toList() : null; List influencerFilters = placesFilterParamsCommand.isInfluencerFilterExists() @@ -73,7 +82,7 @@ public Page getPlacesWithinRadius( List placeInfos = convertToPlaceInfos(placesPage, placeIdToInfluencerName); // PlaceInfo 리스트를 Page로 변환하여 반환 - return new PageImpl<>(placeInfos, placesPage.getPageable(), placeInfos.size()); + return new PageImpl<>(placeInfos, placesPage.getPageable(), placesPage.getTotalElements()); } private List convertToPlaceInfos(Page placesPage, @@ -131,7 +140,11 @@ public PlaceDetailInfo getPlaceDetailInfo(Long placeId) { video = videos.get(0); } Influencer influencer = (video != null) ? video.getInfluencer() : null; - return PlaceDetailInfo.from(place, influencer, video, isLikedPlace(place.getId())); + + Integer numOfLikes = reviewRepository.countByPlaceIdAndIsLikedTrue(placeId); + Integer numOfDislikes = reviewRepository.countByPlaceIdAndIsLikedFalse(placeId); + return PlaceDetailInfo.from(place, influencer, video, isLikedPlace(place.getId()), + numOfLikes, numOfDislikes); } public List createPlaces(List placeCommands) { @@ -214,4 +227,22 @@ public PlaceMessageCommand getPlaceMessageCommand(Long placeId) { return PlaceMessageCommand.of(place, influencer, video); } + + public Page getLikedPlaceInfo(Long userId, Pageable pageable) { + Page placePage = likedPlaceRepository.findByUserIdAndIsLikedTrueWithPlace( + userId, pageable); + List placeIds = placePage.map(likedPlace -> likedPlace.getPlace().getId()).toList(); + List