diff --git a/.ebextensions-dev/00-makeFiles.config b/.ebextensions-dev/00-makeFiles.config new file mode 100644 index 0000000..4e3feb0 --- /dev/null +++ b/.ebextensions-dev/00-makeFiles.config @@ -0,0 +1,12 @@ +files: + "/sbin/appstart": + mode: "000755" + owner: webapp + group: webapp + content: | + #!/usr/bin/env bash + JAR_PATH=/var/app/current/application.jar + + # run app + killalljava + java -Dfile.encoding=UTF-8 -jar $JAR_PATH --spring.profiles.active=dev \ No newline at end of file diff --git a/.ebextensions-dev/01-set-timezone.config b/.ebextensions-dev/01-set-timezone.config new file mode 100644 index 0000000..869275c --- /dev/null +++ b/.ebextensions-dev/01-set-timezone.config @@ -0,0 +1,3 @@ +commands: + set_time_zone: + command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime \ No newline at end of file diff --git a/.ebextensions-prod/00-makeFiles.config b/.ebextensions-prod/00-makeFiles.config new file mode 100644 index 0000000..92a5339 --- /dev/null +++ b/.ebextensions-prod/00-makeFiles.config @@ -0,0 +1,12 @@ +files: + "/sbin/appstart": + mode: "000755" + owner: webapp + group: webapp + content: | + #!/usr/bin/env bash + JAR_PATH=/var/app/current/application.jar + + # run app + killalljava + java -Dfile.encoding=UTF-8 -jar $JAR_PATH --spring.profiles.active=prod \ No newline at end of file diff --git a/.ebextensions-prod/01-set-timezone.config b/.ebextensions-prod/01-set-timezone.config new file mode 100644 index 0000000..869275c --- /dev/null +++ b/.ebextensions-prod/01-set-timezone.config @@ -0,0 +1,3 @@ +commands: + set_time_zone: + command: ln -f -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime \ No newline at end of file diff --git "a/.github/ISSUE_TEMPLATE/\342\234\250feat.md" "b/.github/ISSUE_TEMPLATE/\342\234\250feat.md" new file mode 100644 index 0000000..534e0c3 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\342\234\250feat.md" @@ -0,0 +1,24 @@ +--- +name: "✨Feat" +about: feat +title: "[FEAT]" +labels: "✨feature" +assignees: '' + +--- + +:exclamation:**이슈내용** +--- +content + +:star:**상세 내용** +--- +- content + +:white_check_mark:**체크리스트** +--- +- [ ] content + +:mag_right:**레퍼런스** +--- +❌ diff --git "a/.github/ISSUE_TEMPLATE/\360\237\206\230help.md" "b/.github/ISSUE_TEMPLATE/\360\237\206\230help.md" new file mode 100644 index 0000000..166b5a2 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\206\230help.md" @@ -0,0 +1,20 @@ +--- +name: "\U0001F198Help" +about: I got trouble, help me please +title: "[HELP]" +labels: help wanted +assignees: '' + +--- + +:question:**이슈 내용** +--- +content + +:mag_right:**경로** +--- +content + +:sparkles:**의도한 결과** +--- +content diff --git "a/.github/ISSUE_TEMPLATE/\360\237\220\233bug-report.md" "b/.github/ISSUE_TEMPLATE/\360\237\220\233bug-report.md" new file mode 100644 index 0000000..3325e4f --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\220\233bug-report.md" @@ -0,0 +1,27 @@ +--- +name: "\U0001F41BBug report" +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +:rage:**오류 설명** +A clear and concise description of what the bug is. + +--- +:mag_right:**오류 발생 상황** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +--- +:grin:**의도한 결과** +A clear and concise description of what you expected to happen. + +--- +:fireworks:**스크린샷** +If applicable, add screenshots to help explain your problem. diff --git "a/.github/ISSUE_TEMPLATE/\360\237\232\221fix.md" "b/.github/ISSUE_TEMPLATE/\360\237\232\221fix.md" new file mode 100644 index 0000000..e29fd04 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\360\237\232\221fix.md" @@ -0,0 +1,20 @@ +--- +name: "\U0001F691Fix" +about: I fix bug +title: "[FIX]" +labels: '' +assignees: '' + +--- + +:mag_right:**기존 문제점 및 해결 방안** +--- +content + +:wrench:**수정한 내용** +--- +content + +:fireworks:**스크린샷** +--- +content diff --git a/.github/workflows/develop_dev.yml b/.github/workflows/develop_dev.yml new file mode 100644 index 0000000..aa9465a --- /dev/null +++ b/.github/workflows/develop_dev.yml @@ -0,0 +1,75 @@ +name: Swacademy Dev CI/CD + +on: + pull_request: + types: [closed] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'develop' + + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'adopt' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash + + + - name: Build with Gradle + run: ./gradlew clean build -x test + shell: bash + + + - name: Get current time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH-mm-ss + utcOffset: "+09:00" + + - name: Show Current Time + run: echo "CurrentTime=$" + shell: bash + + + + + - name: Generate deployment package + run: | + mkdir -p deploy + cp build/libs/*.jar deploy/application.jar + cp Procfile deploy/Procfile + cp -r .ebextensions-dev deploy/.ebextensions + cp -r .platform deploy/.platform + cd deploy && zip -r deploy.zip . + + + - name: Debug Beanstalk Deploy + run: | + echo "AWS_ACTION_ACCESS_KEY_ID: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }}" + echo "AWS_ACTION_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }}" + + - name: Beanstalk Deploy + uses: einaregilsson/beanstalk-deploy@v20 + with: + aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} + aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} + application_name: swacademy-dev + environment_name: Swacademy-dev-env + version_label: github-action-${{ steps.current-time.outputs.formattedTime }} + region: ap-northeast-2 + deployment_package: deploy/deploy.zip + wait_for_deployment: false + + diff --git a/.github/workflows/develop_prod.yml b/.github/workflows/develop_prod.yml new file mode 100644 index 0000000..b27b08d --- /dev/null +++ b/.github/workflows/develop_prod.yml @@ -0,0 +1,75 @@ +name: Swacademy Prod CI/CD + +on: + pull_request: + types: [closed] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' + + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'adopt' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + shell: bash + + + - name: Build with Gradle + run: ./gradlew clean build -x test + shell: bash + + + - name: Get current time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH-mm-ss + utcOffset: "+09:00" + + - name: Show Current Time + run: echo "CurrentTime=$" + shell: bash + + + + + - name: Generate deployment package + run: | + mkdir -p deploy + cp build/libs/*.jar deploy/application.jar + cp Procfile deploy/Procfile + cp -r .ebextensions-prod deploy/.ebextensions + cp -r .platform deploy/.platform + cd deploy && zip -r deploy.zip . + + + - name: Debug Beanstalk Deploy + run: | + echo "AWS_ACTION_ACCESS_KEY_ID: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }}" + echo "AWS_ACTION_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }}" + + - name: Beanstalk Deploy + uses: einaregilsson/beanstalk-deploy@v20 + with: + aws_access_key: ${{ secrets.AWS_ACTION_ACCESS_KEY_ID }} + aws_secret_key: ${{ secrets.AWS_ACTION_SECRET_ACCESS_KEY }} + application_name: swacademy-prod + environment_name: Swacademy-prod-env + version_label: github-action-${{ steps.current-time.outputs.formattedTime }} + region: ap-northeast-2 + deployment_package: deploy/deploy.zip + wait_for_deployment: false + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8608663 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +src/main/generated/ diff --git a/.platform/nginx.conf b/.platform/nginx.conf new file mode 100644 index 0000000..c3c72a1 --- /dev/null +++ b/.platform/nginx.conf @@ -0,0 +1,67 @@ +user nginx; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; +worker_processes auto; +worker_rlimit_nofile 33282; + +events { + use epoll; + worker_connections 1024; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + + types_hash_max_size 2048; + types_hash_bucket_size 128; + + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + include conf.d/*.conf; + + map $http_upgrade $connection_upgrade { + default "upgrade"; + } + + upstream springboot { + server 127.0.0.1:8080; + keepalive 1024; + } + + server { + listen 80 default_server; + listen [::]:80 default_server; + + location / { + proxy_pass http://springboot; + # CORS 관련 헤더 추가 + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type'; + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + access_log /var/log/nginx/access.log main; + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + gzip off; + gzip_comp_level 4; + + # Include the Elastic Beanstalk generated locations + include conf.d/elasticbeanstalk/healthd.conf; + } +} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..58dab8d --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: appstart \ No newline at end of file diff --git a/README.md b/README.md index 299347f..32ebe6b 100644 --- a/README.md +++ b/README.md @@ -1 +1,206 @@ -# SWAcademy-Server \ No newline at end of file +# SWAcademy-Server + +탄소중립 sw아카데미 다회용기팀 서버 + +## 👥 Server 팀원 +|이희주|황규혁|김동한| +|:-:|:-:|:-:| +|희주||동한 +|[@hj1487](https://github.com/hj1487)|[@Gyuhyeok99](https://github.com/Gyuhyeok99)|[@kdhan235](https://github.com/kdhan235)| + + +## 🌟 담당 역할 +|담당 역할|Role| +|:-:|:-:| +|Nginx 배포, CI/CD 구축|황규혁| +|DB 구축 (RDS)|황규혁| +|ERD 작성|이희주, 황규혁, 김동한| +|API 개발|황규혁| + +## 🛠️ 개발 환경 +||| +|:-:|:-:| +|통합 개발 환경|IntelliJ| +|Spring 버전|3.2.4| +|데이터베이스|AWS RDS(MySQL), ElastiCache(Redis)| +|배포|AWS Elastic beanstalk, EC2| +|Project 빌드 관리 도구|Gradle| +|Java version|java 17| +|패키지 구조|도메인 패키지 구조| +|API 테스트|PostMan, Swagger(https://dev.swacademy.store/swagger-ui/index.html#/)| + +## 🔧 시스템 아키텍처 +아키텍처 + + + +## 📜 API Docs +🔗 API Docs + +## ☁️ERD +erd + + +## ✨Structure +```text +api-server-spring-boot + > .ebextensions-dev // dev 서버 관련 ci/cd 구축 + | 00-makeFiles.config + | 01-set-timezone.config + > .ebextensions-prod // prod 서버 관련 ci/cd 구축 + | 00-makeFiles.config + | 01-set-timezone.config + > .github + > ISSUE_TEMPLATE + | ✨feat.md + | 🆘help.md + | 🐛bug-report.md + | 🚑fix.md + > worksflows + | develop_dev.yml // dev 서버 github action을 위한 파일 + | develop_prod.yml // prod 서버 github action을 위한 파일 + > .platform + | nginx.conf // nginx 설정 + > * build + > gradle + > src.main.java.carbonneutral.academy + > api + > controller + > auth + > dto + > request + > response + | AuthController.java + > point + > dto + > request + > response + | PointController.java + > use + > dto + > request + > response + | UseController.java + > user + > dto + > request + > response + | UserController.java + > converter + > auth + | AuthConverter.java + > time + | TimeConverter.java + > use + | UseConverter.java + > user + | UserConverter.java + > service + > auth + > social // 소셜 로그인 관련 + > kakao + | KakaoLoginService.java + | KakaoLoginServiceImpl.java + | AuthService.java + | AuthServiceImpl.java + > point + | PointService.java + | PointServiceImpl.java + > use + | UseService.java + | UseServiceImpl.java + > user + | UserService.java + | UserServiceImpl.java + > common + > code + > status + | ErrorStatus.java // 에러 응답 메시지 모아놓은 곳 + | SuccessStatus.java // 성공 응답 메시지 모아놓은 곳 + | BaseCode.java + | BaseErrorCode.java + | ErrorReasonDTO.java + | ReasonDTO.java + > config + | AppConfig.java + | RedisConfig.java // 레디스 관련 설정 + | Security.config.java // Spring Security 관련 설정 + | SwaggerConfig.java // Swagger 관련 설정 + > exceptions + > handler + | ExceptionHandler.java + | BaseException.java // Controller, Service에서 Response 용으로 공통적으로 사용 될 익셉션 클래스 + | ExceptionAdvice.java // ExceptionHandler를 활용하여 정의해놓은 예외처리를 통합 관리하는 클래스 + | BaseEntity.java // create, update, state 등 Entity에 공통적으로 정의되는 변수를 정의한 BaseEntity + | BaseResponse.java // Controller 에서 Response 용으로 공통적으로 사용되는 구조를 위한 모델 클래스 + > domain + > location + > enums + | LocationType.java + > repository + | LocationJpaRepository + | Location.java + > mapping + > repository + | LocationContainerJpaRepository.java + | LocationContainer.java + > multi_use_container + > repository + | MultiUseContainerRepository.java + | MultiUseContainer.java + > point + > repository + | PointJpaRepository.java + | Point.java + > use + > enums + | UseStatus.java + > repository + | UseJpaRepository.java + | UseQueryRepository // 통계 쿼리를 위한 querydsl용 repository + | Use.java + > user + > enums + | Permission.java //권한 부여 + | Role.java // 유저 역할 + | SocialType.java + > repository + | UserJpaRepository.java + | User.java + > utils + > jwt // jwt 관련 + | JwtAuthenticationFilter.java + | JwtProvider.java + | LogoutService.java // 로그아웃 + | ApplicationAuditAware.java + | RedisProvider.java // 레디스 서비스 + | AcademyApplication // SpringBootApplication 서버 시작 지점 + > resources + | application.yml // Database 연동을 위한 설정 값 세팅 및 Port 정의 파일 + | application-dev.yml // dev 연동 + | application-local.yml // local 연동 +build.gradle // gradle 빌드시에 필요한 dependency 설정 +.gitignore // git 에 포함되지 않아야 하는 폴더, 파일들을 작성 + +``` +## 환경 설정 내역 +- Local 실행 시 + - 실행 방법: 프로젝트를 로컬 환경에서 실행할 때는 환경 변수 또는 설정 파일을 통해 local 모드로 설정 + - 서버 접속 주소: localhost:8080 에서 서버 실행 + - 데이터베이스 접속: 로컬에 설치된 MySQL 데이터베이스에 접속, 로컬 데이터베이스의 접속 정보(호스트, 포트, 사용자 이름, 비밀번호 등)는 개발 환경에 맞게 설정. + - 캐시 서버 접속: 로컬에서 실행 중인 Redis 인스턴스에 접속, 로컬 Redis 서버의 접속 정보를 환경에 맞게 설정. + +- Dev 실행 시 + - 실행 방법: 개발 환경에서는 환경 변수 또는 설정 파일을 dev 모드로 설정 + - 서버 접속 주소: 프로젝트는 https://dev.swacademy.store/ 주소를 통해 접근 + - 데이터베이스 접속: AWS RDS(MySQL) 인스턴스에 접속, 개발 환경에 맞는 RDS 인스턴스의 접속 정보(엔드포인트, 포트, 사용자 이름, 비밀번호 등)를 설정 파일에 명시해야 함. + - 캐시 서버 접속: AWS ElastiCache(Redis) 인스턴스에 접속, 개발 환경에 맞는 ElastiCache 인스턴스의 접속 정보(엔드포인트, 포트 등)를 설정 파일에 명시해야 함. + +- Prod 서버 아직 존재 x + +## 🌱 Branch +- main : 최종 +- develop : 개발 +- feat : 기능 개발 +- refactor : 기능 수정 +- ci : ci/cd 구축 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..c7a85ff --- /dev/null +++ b/build.gradle @@ -0,0 +1,79 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.4' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'carbonneutral' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + // for test + testImplementation 'org.projectlombok:lombok' + testImplementation "org.mockito:mockito-core:3.3.3" + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + + //jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + + //oauth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache' + + // Querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + //s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + implementation 'org.json:json:20210307' + + +} + +tasks.named('test') { + useJUnitPlatform() +} + +clean { + delete file('src/main/generated') +} + + +jar { + enabled = false +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..082c781 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'academy' diff --git a/src/main/java/carbonneutral/academy/AcademyApplication.java b/src/main/java/carbonneutral/academy/AcademyApplication.java new file mode 100644 index 0000000..b75f868 --- /dev/null +++ b/src/main/java/carbonneutral/academy/AcademyApplication.java @@ -0,0 +1,15 @@ +package carbonneutral.academy; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@SpringBootApplication +@EnableJpaAuditing(auditorAwareRef = "auditorAware") +public class AcademyApplication { + + public static void main(String[] args) { + SpringApplication.run(AcademyApplication.class, args); + } + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/auth/AuthController.java b/src/main/java/carbonneutral/academy/api/controller/auth/AuthController.java new file mode 100644 index 0000000..ea5616a --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/auth/AuthController.java @@ -0,0 +1,40 @@ +package carbonneutral.academy.api.controller.auth; + +import carbonneutral.academy.api.controller.auth.dto.request.PostLoginReq; +import carbonneutral.academy.api.controller.auth.dto.response.PostSocialRes; +import carbonneutral.academy.api.service.auth.AuthService; +import carbonneutral.academy.common.BaseResponse; +import carbonneutral.academy.domain.user.enums.SocialType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static carbonneutral.academy.common.code.status.SuccessStatus.OAUTH_OK; + +@Slf4j +@Tag(name = "auth controller", description = "인증 필요 없는 API") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/auth") +public class AuthController { + + private final AuthService authService; + + @GetMapping("/health") + @Operation(summary = "서버 상태 확인 API",description = "서버 상태를 확인합니다.") + public BaseResponse health() { + return BaseResponse.onSuccess("I'm healthy"); + } + + @PostMapping("/login") + @Operation(summary = "소셜 로그인 API",description = "소셜 로그인을 진행합니다.") + public BaseResponse login(@Validated @RequestBody PostLoginReq postLoginReq) { + SocialType socialType = SocialType.valueOf(postLoginReq.getSocialType().toUpperCase()); + return BaseResponse.of(OAUTH_OK, authService.socialLogin(postLoginReq.getCode(), socialType)); + } + + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/auth/dto/request/PatchAdditionalInfoReq.java b/src/main/java/carbonneutral/academy/api/controller/auth/dto/request/PatchAdditionalInfoReq.java new file mode 100644 index 0000000..f599689 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/auth/dto/request/PatchAdditionalInfoReq.java @@ -0,0 +1,21 @@ +package carbonneutral.academy.api.controller.auth.dto.request; + + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PatchAdditionalInfoReq { + + @NotNull + private String nickname; + + @NotNull + private boolean gender; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/auth/dto/request/PostLoginReq.java b/src/main/java/carbonneutral/academy/api/controller/auth/dto/request/PostLoginReq.java new file mode 100644 index 0000000..42bb7dc --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/auth/dto/request/PostLoginReq.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.api.controller.auth.dto.request; + +import carbonneutral.academy.domain.user.enums.SocialType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PostLoginReq { + + private String socialType; + private String code; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/GetKakaoRes.java b/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/GetKakaoRes.java new file mode 100644 index 0000000..516c7a6 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/GetKakaoRes.java @@ -0,0 +1,19 @@ +package carbonneutral.academy.api.controller.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetKakaoRes { + + String name; + String id; + +} + + diff --git a/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/PatchAdditionalInfoRes.java b/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/PatchAdditionalInfoRes.java new file mode 100644 index 0000000..a36ba78 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/PatchAdditionalInfoRes.java @@ -0,0 +1,16 @@ +package carbonneutral.academy.api.controller.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PatchAdditionalInfoRes { + + private int id; + private boolean isFinished; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/PostSocialRes.java b/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/PostSocialRes.java new file mode 100644 index 0000000..55236ac --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/auth/dto/response/PostSocialRes.java @@ -0,0 +1,21 @@ +package carbonneutral.academy.api.controller.auth.dto.response; + +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PostSocialRes { + + @NotNull + private int id; + + private boolean isFinished; + @NotNull + private String accessToken; + @NotNull + private String refreshToken; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/ocr/OcrController.java b/src/main/java/carbonneutral/academy/api/controller/ocr/OcrController.java new file mode 100644 index 0000000..55d1266 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/ocr/OcrController.java @@ -0,0 +1,37 @@ +package carbonneutral.academy.api.controller.ocr; + + +import carbonneutral.academy.api.controller.ocr.dto.response.PostOcrRes; +import carbonneutral.academy.api.service.ocr.OcrService.OcrService; +import carbonneutral.academy.common.BaseResponse; +import carbonneutral.academy.domain.user.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import static carbonneutral.academy.common.code.status.SuccessStatus.OCR_OK; + +@Slf4j +@Tag(name = "ocr controller", description = "ocr 관련 API") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/ocr") +public class OcrController { + + private final OcrService ocrService; + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "OCR 이미지 인식 API",description = "OCR 이미지를 인식합니다.") + public BaseResponse ocrImage(@AuthenticationPrincipal User user + , @RequestParam("receipt") MultipartFile receipt) { + return BaseResponse.of(OCR_OK, ocrService.ocrImage(user, receipt)); + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/ocr/dto/response/PostOcrRes.java b/src/main/java/carbonneutral/academy/api/controller/ocr/dto/response/PostOcrRes.java new file mode 100644 index 0000000..963e541 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/ocr/dto/response/PostOcrRes.java @@ -0,0 +1,23 @@ +package carbonneutral.academy.api.controller.ocr.dto.response; + +import carbonneutral.academy.domain.use.enums.UseStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PostOcrRes { + + private int useLocationId; + private String useLocationName; + private String useLocationAddress; + private String useLocationImageUrl; + private String useTime; + private int currentPoint; + private int acquiredPoint; + private int userId; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/point/PointController.java b/src/main/java/carbonneutral/academy/api/controller/point/PointController.java new file mode 100644 index 0000000..7a39039 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/point/PointController.java @@ -0,0 +1,44 @@ +package carbonneutral.academy.api.controller.point; + +import carbonneutral.academy.api.controller.point.dto.response.GetBasePointRes; +import carbonneutral.academy.api.service.point.PointService; +import carbonneutral.academy.common.BaseResponse; +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.domain.user.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Slice; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static carbonneutral.academy.common.code.status.ErrorStatus.INVALID_PAGE; +import static carbonneutral.academy.common.code.status.ErrorStatus.INVALID_SIZE_10; +import static carbonneutral.academy.common.code.status.SuccessStatus.GET_POINT_OK; + +@Slf4j +@Tag(name = "point controller", description = "다회용기 포인트 관련 API") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/points") +public class PointController { + + private final PointService pointService; + + @GetMapping + @Operation(summary = "유저 포인트 내역 조회 API",description = "유저의 포인트 내역을 조회합니다. page는 0부터 시작합니다. size는 10으로 요청해야합니다.") + BaseResponse> getPoints(@AuthenticationPrincipal User user, @RequestParam("page") Integer page, @RequestParam("size") Integer size) { + if (page < 0) { + throw new BaseException(INVALID_PAGE); + } + if (size != 10) { + throw new BaseException(INVALID_SIZE_10); + } + return BaseResponse.of(GET_POINT_OK, pointService.getPoints(user, page, size)); + } + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetAccumulatePointRes.java b/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetAccumulatePointRes.java new file mode 100644 index 0000000..69417db --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetAccumulatePointRes.java @@ -0,0 +1,20 @@ +package carbonneutral.academy.api.controller.point.dto.response; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor +@SuperBuilder +@AllArgsConstructor +public class GetAccumulatePointRes extends GetBasePointRes{ + + private String rentalLocationName; + private String rentalLocationAddress; + private String returnLocationName; + private String returnLocationAddress; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetBasePointRes.java b/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetBasePointRes.java new file mode 100644 index 0000000..b992253 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetBasePointRes.java @@ -0,0 +1,21 @@ +package carbonneutral.academy.api.controller.point.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor +@SuperBuilder +@AllArgsConstructor +public class GetBasePointRes { + + protected String useAt; + protected String useAtParsed; + protected int point; + protected String pointTypeImageUrl; + + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetUtilizedPointRes.java b/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetUtilizedPointRes.java new file mode 100644 index 0000000..0865d54 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/point/dto/response/GetUtilizedPointRes.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.api.controller.point.dto.response; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@NoArgsConstructor +@SuperBuilder +@AllArgsConstructor +public class GetUtilizedPointRes extends GetBasePointRes { + + private String useLocationName; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/UseController.java b/src/main/java/carbonneutral/academy/api/controller/use/UseController.java new file mode 100644 index 0000000..0085d6b --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/UseController.java @@ -0,0 +1,63 @@ +package carbonneutral.academy.api.controller.use; + +import carbonneutral.academy.api.controller.use.dto.request.PatchReturnReq; +import carbonneutral.academy.api.controller.use.dto.request.PostUseReq; +import carbonneutral.academy.api.controller.use.dto.response.*; +import carbonneutral.academy.api.service.use.UseService; +import carbonneutral.academy.common.BaseResponse; +import carbonneutral.academy.domain.user.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + + + +import static carbonneutral.academy.common.code.status.SuccessStatus.*; + +@Slf4j +@Tag(name = "use controller", description = "다회용기 이용 관련 API") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/uses") +public class UseController { + + private final UseService useService; + + //유저가 이용중인 다회용기 조회 API List 이용 + @GetMapping + @Operation(summary = "유저가 이용중인 다회용기 조회 API",description = "유저가 이용중인 다회용기를 조회합니다.") + BaseResponse getInUsesMultipleTimeContainers(@AuthenticationPrincipal User user) { + return BaseResponse.of(IN_USES_OK, useService.getInUsesMultipleTimeContainers(user)); + } + + @GetMapping("{useAt}") + @Operation(summary = "유저가 이용중인 다회용기 단일 조회 API",description = "유저가 이용중인 다회용기를 단일 조회합니다.") + BaseResponse getInUseMultipleTimeContainer(@AuthenticationPrincipal User user, @PathVariable("useAt") String useAt) { + return BaseResponse.of(IN_USE_OK, useService.getInUseMultipleTimeContainer(user, useAt)); + } + + @GetMapping("/location") + @Operation(summary = "QR 이용 시 해당 장소 조회 API",description = "QR 인증을 통해 해당 장소를 조회합니다.") + BaseResponse getLocation(@RequestParam("locationId") int locationId, + @RequestParam("point")int point) { + return BaseResponse.of(LOCATION_OK, useService.getLocation(locationId, point)); + } + @PostMapping + @Operation(summary = "다회용기 이용 시 API",description = "앱에서 QR 인증을 통해 다회용기를 이용합니다.") + BaseResponse useMultipleTimeContainers(@AuthenticationPrincipal User user, + @Validated @RequestBody PostUseReq postUseReq) { + return BaseResponse.of(USE_SAVE_OK, useService.useMultipleTimeContainers(user, postUseReq)); + } + + @PatchMapping("{useAt}") + @Operation(summary = "다회용기 반납 API", description = "앱에서 QR 인증을 통해 다회용기를 반납합니다.") + BaseResponse returnMultipleTimeContainers(@AuthenticationPrincipal User user, + @Validated @RequestBody PatchReturnReq patchReturnReq, @PathVariable("useAt") String useAt) { + return BaseResponse.of(RETURN_SAVE_OK, useService.returnMultipleTimeContainers(user, patchReturnReq, useAt)); + } + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/request/PatchReturnReq.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/request/PatchReturnReq.java new file mode 100644 index 0000000..6572839 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/request/PatchReturnReq.java @@ -0,0 +1,16 @@ +package carbonneutral.academy.api.controller.use.dto.request; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PatchReturnReq { + + private int returnLocationId; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/request/PostUseReq.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/request/PostUseReq.java new file mode 100644 index 0000000..14204b7 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/request/PostUseReq.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.api.controller.use.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PostUseReq { + + private int locationId; + private int point; + private int multiUseContainerId; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetHomeRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetHomeRes.java new file mode 100644 index 0000000..db70a73 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetHomeRes.java @@ -0,0 +1,21 @@ +package carbonneutral.academy.api.controller.use.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetHomeRes { + + private int userId; + private String nickname; + private List getUseResList; + private int useCount; + private int currentPoint; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetLocationRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetLocationRes.java new file mode 100644 index 0000000..aff3e3e --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetLocationRes.java @@ -0,0 +1,24 @@ +package carbonneutral.academy.api.controller.use.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetLocationRes { + private int locationId; + private String locationName; + private String locationAddress; + private String locationImageUrl; + private BigDecimal latitude; + private BigDecimal longitude; + private int point; + private List multiUseContainerIdList; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetMyPageRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetMyPageRes.java new file mode 100644 index 0000000..0d4f1d3 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetMyPageRes.java @@ -0,0 +1,28 @@ +package carbonneutral.academy.api.controller.use.dto.response; + + +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyStatisticsRes; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetMyPageRes { + + private String nickname; + + private boolean gender; + + private int currentPoint; + private int totalUseCount; + private int totalReturnCount; + private List dailyStatisticsResList; + private List monthlyStatisticsResList; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetReturnRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetReturnRes.java new file mode 100644 index 0000000..f91d27e --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetReturnRes.java @@ -0,0 +1,25 @@ +package carbonneutral.academy.api.controller.use.dto.response; + +import carbonneutral.academy.domain.location.enums.LocationType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetReturnRes { + + private int locationId; + private String name; + private String address; + private BigDecimal latitude; + private BigDecimal longitude; + private LocationType locationType; + private String imageUrl; + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetUseDetailRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetUseDetailRes.java new file mode 100644 index 0000000..f91ce4b --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetUseDetailRes.java @@ -0,0 +1,25 @@ +package carbonneutral.academy.api.controller.use.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetUseDetailRes { + + private int rentalLocationId; + private String locationImageUrl; + private String locationName; + private String locationAddress; + private String useAt; + private int point; + private int multiUseContainerId; + private List getReturnResList; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetUseRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetUseRes.java new file mode 100644 index 0000000..1628cd2 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/GetUseRes.java @@ -0,0 +1,23 @@ +package carbonneutral.academy.api.controller.use.dto.response; + +import carbonneutral.academy.domain.use.enums.UseStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetUseRes { + + private int rentalLocationId; + private String locationImageUrl; + private String locationName; + private String useAt; + private UseStatus status; + private int multiUseContainerId; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/PatchReturnRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/PatchReturnRes.java new file mode 100644 index 0000000..8fdda19 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/PatchReturnRes.java @@ -0,0 +1,23 @@ +package carbonneutral.academy.api.controller.use.dto.response; + + +import carbonneutral.academy.domain.use.enums.UseStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PatchReturnRes { + private int returnLocationId; + private String returnLocationName; + private String returnLocationAddress; + private String returnTime; + private int currentPoint; + private int acquiredPoint; + private int userId; + private UseStatus status; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/PostUseRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/PostUseRes.java new file mode 100644 index 0000000..256a0bb --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/PostUseRes.java @@ -0,0 +1,28 @@ +package carbonneutral.academy.api.controller.use.dto.response; + +import carbonneutral.academy.domain.use.enums.UseStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PostUseRes { + private String useAt; + private int point; + private int userId; + private int locationId; + private String locationName; + private String locationAddress; + private String locationImageUrl; + private BigDecimal latitude; + private BigDecimal longitude; + private int multiUseContainerId; + private UseStatus status; + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyReturnStatisticsRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyReturnStatisticsRes.java new file mode 100644 index 0000000..d7ece32 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyReturnStatisticsRes.java @@ -0,0 +1,22 @@ +package carbonneutral.academy.api.controller.use.dto.response.statistics.daily; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +public class GetDailyReturnStatisticsRes { + + private int dayOfWeek; + private int returnCount; + + @QueryProjection + public GetDailyReturnStatisticsRes(int dayOfWeek, int returnCount) { + this.dayOfWeek = dayOfWeek; + this.returnCount = returnCount; + } + +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyStatisticsRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyStatisticsRes.java new file mode 100644 index 0000000..5869783 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyStatisticsRes.java @@ -0,0 +1,23 @@ +package carbonneutral.academy.api.controller.use.dto.response.statistics.daily; + + +import lombok.*; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetDailyStatisticsRes { + + private int dayOfWeek; + private int useCount; + private int returnCount; + + public void setUseCount(int useCount) { + this.useCount = useCount; + } + + public void setReturnCount(int returnCount) { + this.returnCount = returnCount; + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyUseStatisticsRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyUseStatisticsRes.java new file mode 100644 index 0000000..8e00c66 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/daily/GetDailyUseStatisticsRes.java @@ -0,0 +1,21 @@ +package carbonneutral.academy.api.controller.use.dto.response.statistics.daily; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +public class GetDailyUseStatisticsRes { + + private int dayOfWeek; + private int useCount; + + @QueryProjection + public GetDailyUseStatisticsRes(int dayOfWeek, int useCount) { + this.dayOfWeek = dayOfWeek; + this.useCount = useCount; + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyReturnStatisticsRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyReturnStatisticsRes.java new file mode 100644 index 0000000..c74b194 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyReturnStatisticsRes.java @@ -0,0 +1,20 @@ +package carbonneutral.academy.api.controller.use.dto.response.statistics.monthly; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@Builder +public class GetMonthlyReturnStatisticsRes { + + private int month; + private int returnCount; + + @QueryProjection + public GetMonthlyReturnStatisticsRes(int month, int returnCount) { + this.month = month; + this.returnCount = returnCount; + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyStatisticsRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyStatisticsRes.java new file mode 100644 index 0000000..0858ca1 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyStatisticsRes.java @@ -0,0 +1,23 @@ +package carbonneutral.academy.api.controller.use.dto.response.statistics.monthly; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class GetMonthlyStatisticsRes { + + private int month; + private int useCount; + private int returnCount; + + public void setUseCount(int useCount) { + this.useCount = useCount; + } + + public void setReturnCount(int returnCount) { + this.returnCount = returnCount; + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyUseStatisticsRes.java b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyUseStatisticsRes.java new file mode 100644 index 0000000..5b34767 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/use/dto/response/statistics/monthly/GetMonthlyUseStatisticsRes.java @@ -0,0 +1,20 @@ +package carbonneutral.academy.api.controller.use.dto.response.statistics.monthly; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@Builder +public class GetMonthlyUseStatisticsRes { + + private int month; + private int useCount; + + @QueryProjection + public GetMonthlyUseStatisticsRes(int month, int useCount) { + this.month = month; + this.useCount = useCount; + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/user/UserController.java b/src/main/java/carbonneutral/academy/api/controller/user/UserController.java new file mode 100644 index 0000000..f9c9cac --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/user/UserController.java @@ -0,0 +1,48 @@ +package carbonneutral.academy.api.controller.user; + +import carbonneutral.academy.api.controller.auth.dto.request.PatchAdditionalInfoReq; +import carbonneutral.academy.api.controller.auth.dto.response.PatchAdditionalInfoRes; +import carbonneutral.academy.api.controller.use.dto.response.GetMyPageRes; +import carbonneutral.academy.api.controller.user.dto.request.PatchInfoReq; +import carbonneutral.academy.api.controller.user.dto.response.PatchInfoRes; +import carbonneutral.academy.common.BaseResponse; +import carbonneutral.academy.api.service.user.UserService; +import carbonneutral.academy.domain.user.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static carbonneutral.academy.common.code.status.SuccessStatus.*; + +@Slf4j +@Tag(name = "user controller", description = "유저 관련 API") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserController { + + private final UserService userService; + + @PatchMapping("/additional-info") + @Operation(summary = "추가정보 입력 API", description = "추가정보 입력을 진행합니다.") + public BaseResponse additionalInfo(@AuthenticationPrincipal User user, @Validated @RequestBody PatchAdditionalInfoReq request) { + return BaseResponse.of(ADDITIONAL_INFO_OK, userService.additionalInfo(user, request)); + } + + @GetMapping("/my-page") + @Operation(summary = "마이페이지 조회 API", description = "마이페이지를 조회합니다.") + public BaseResponse mypage(@AuthenticationPrincipal User user) { + return BaseResponse.of(MYPAGE_OK, userService.mypage(user)); + } + + @PatchMapping("/my-page/edit") + @Operation(summary = "마이페이지 수정 API", description = "마이페이지를 수정합니다.") + public BaseResponse mypageEdit(@AuthenticationPrincipal User user, @Validated @RequestBody PatchInfoReq request) { + log.info("request : {}", request.getEditNickname()); + return BaseResponse.of(MYPAGE_EDIT_OK, userService.mypageEdit(user, request)); + } +} diff --git a/src/main/java/carbonneutral/academy/api/controller/user/dto/request/PatchInfoReq.java b/src/main/java/carbonneutral/academy/api/controller/user/dto/request/PatchInfoReq.java new file mode 100644 index 0000000..586f955 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/user/dto/request/PatchInfoReq.java @@ -0,0 +1,19 @@ +package carbonneutral.academy.api.controller.user.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PatchInfoReq { + + + + @NotNull + private String editNickname; +} diff --git a/src/main/java/carbonneutral/academy/api/controller/user/dto/response/PatchInfoRes.java b/src/main/java/carbonneutral/academy/api/controller/user/dto/response/PatchInfoRes.java new file mode 100644 index 0000000..31237d2 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/controller/user/dto/response/PatchInfoRes.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.api.controller.user.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@AllArgsConstructor +public class PatchInfoRes { + + private String editNickname; + + +} diff --git a/src/main/java/carbonneutral/academy/api/converter/auth/AuthConverter.java b/src/main/java/carbonneutral/academy/api/converter/auth/AuthConverter.java new file mode 100644 index 0000000..41d891c --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/converter/auth/AuthConverter.java @@ -0,0 +1,51 @@ +package carbonneutral.academy.api.converter.auth; + +import carbonneutral.academy.api.controller.auth.dto.response.GetKakaoRes; +import carbonneutral.academy.api.controller.auth.dto.response.PostSocialRes; +import carbonneutral.academy.domain.point.Point; +import carbonneutral.academy.domain.use_statistics.UseStatistics; +import carbonneutral.academy.domain.user.User; +import carbonneutral.academy.domain.user.enums.SocialType; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import static carbonneutral.academy.domain.user.enums.Role.USER; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class AuthConverter { + + public static User toUser(GetKakaoRes getKakaoRes){ + return User.builder() + .username(getKakaoRes.getId()) + .socialType(SocialType.KAKAO) + .role(USER) + .build(); + } + public static PostSocialRes toPostSocialRes(User user, String accessToken, String refreshToken){ + return PostSocialRes.builder() + .id(user.getId()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .isFinished(user.isFinished()) + .build(); + + } + + + + public static Point toPoint(User user) { + return Point.builder() + .user(user) + .accumulatedPoint(0) + .utilizedPoint(0) + .build(); + } + + public static UseStatistics toUseStatistics(User user) { + return UseStatistics.builder() + .user(user) + .totalUseCount(0) + .totalReturnCount(0) + .build(); + } +} diff --git a/src/main/java/carbonneutral/academy/api/converter/time/TimeConverter.java b/src/main/java/carbonneutral/academy/api/converter/time/TimeConverter.java new file mode 100644 index 0000000..ea4c85f --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/converter/time/TimeConverter.java @@ -0,0 +1,30 @@ +package carbonneutral.academy.api.converter.time; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TimeConverter { + + public static String toFormattedDate(LocalDateTime localDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss"); + return localDateTime.format(formatter); + } + + public static List toLocalDateTime(String useAt) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss"); + LocalDateTime dateTime = LocalDateTime.parse(useAt, formatter); + LocalDateTime startRange = dateTime; + LocalDateTime endRange = dateTime.withNano(999999999); + return List.of(startRange, endRange); + } + + public static String toMonthDayString(LocalDateTime localDateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 M월 d일"); + return localDateTime.format(formatter); + } +} diff --git a/src/main/java/carbonneutral/academy/api/converter/use/UseConverter.java b/src/main/java/carbonneutral/academy/api/converter/use/UseConverter.java new file mode 100644 index 0000000..b25532a --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/converter/use/UseConverter.java @@ -0,0 +1,121 @@ +package carbonneutral.academy.api.converter.use; + +import carbonneutral.academy.api.controller.use.dto.response.*; +import carbonneutral.academy.api.converter.time.TimeConverter; +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.multi_use_container.MultiUseContainer; +import carbonneutral.academy.domain.point.Point; +import carbonneutral.academy.domain.use.Use; +import carbonneutral.academy.domain.user.User; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +import static carbonneutral.academy.domain.use.enums.UseStatus.USING; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UseConverter { + + public static Use toUse(User user, Location location, int point, int multiUseContainerId) { + return Use.builder() + .useAt(LocalDateTime.now()) + .user(user) + .rentalLocation(location) + .point(point) + .multiUseContainerId(multiUseContainerId) + .returnTime(null) + .status(USING) + .build(); + } + + public static PostUseRes toPostUseRes(Use use, Location location) { + return PostUseRes.builder() + .useAt(TimeConverter.toFormattedDate(use.getUseAt())) + .point(use.getPoint()) + .userId(use.getUser().getId()) + .locationId(location.getId()) + .locationName(location.getName()) + .locationAddress(location.getAddress()) + .locationImageUrl(location.getImageUrl()) + .latitude(location.getLatitude()) + .longitude(location.getLongitude()) + .multiUseContainerId(use.getMultiUseContainerId()) + .status(use.getStatus()) + .build(); + } + + public static GetUseRes toGetUseRes(Use use, Location location, MultiUseContainer multiUseContainer) { + return GetUseRes.builder() + .rentalLocationId(location.getId()) + .locationImageUrl(location.getImageUrl()) + .locationName(location.getName()) + .useAt(TimeConverter.toFormattedDate(use.getUseAt())) + .status(use.getStatus()) + .multiUseContainerId(multiUseContainer.getId()) + .build(); + } + + public static GetHomeRes toGetHomesRes(User user, Point point, List useResList) { + return GetHomeRes.builder() + .userId(user.getId()) + .currentPoint(point.getAccumulatedPoint() - point.getUtilizedPoint()) + .nickname(user.getNickname()) + .getUseResList(useResList) + .useCount(useResList.size()) + .build(); + } + + + public static GetReturnRes toGetReturnRes(Location returnLocation) { + return GetReturnRes.builder() + .locationId(returnLocation.getId()) + .name(returnLocation.getName()) + .address(returnLocation.getAddress()) + .latitude(returnLocation.getLatitude()) + .longitude(returnLocation.getLongitude()) + .locationType(returnLocation.getLocationType()) + .imageUrl(returnLocation.getImageUrl()) + .build(); + } + + public static GetUseDetailRes toGetUseDetailRes(Use use, Location location, List getReturnResList, int multiUseContainerId) { + return GetUseDetailRes.builder() + .rentalLocationId(location.getId()) + .locationImageUrl(location.getImageUrl()) + .locationName(location.getName()) + .locationAddress(location.getAddress()) + .useAt(TimeConverter.toFormattedDate(use.getUseAt())) + .point(use.getPoint()) + .multiUseContainerId(multiUseContainerId) + .getReturnResList(getReturnResList) + .build(); + } + + public static PatchReturnRes toPatchReturnRes(User user, Location returnLocation, Use use, Point point) { + return PatchReturnRes.builder() + .returnLocationId(returnLocation.getId()) + .returnLocationName(returnLocation.getName()) + .returnLocationAddress(returnLocation.getAddress()) + .returnTime(TimeConverter.toFormattedDate(use.getReturnTime())) + .currentPoint(point.getAccumulatedPoint() - point.getUtilizedPoint()) + .acquiredPoint(use.getPoint()) + .userId(user.getId()) + .status(use.getStatus()) + .build(); + } + + public static GetLocationRes toGetLocationRes(Location location, List multiUseContainerIdList, int point) { + return GetLocationRes.builder() + .locationId(location.getId()) + .locationName(location.getName()) + .locationAddress(location.getAddress()) + .locationImageUrl(location.getImageUrl()) + .latitude(location.getLatitude()) + .longitude(location.getLongitude()) + .point(point) + .multiUseContainerIdList(multiUseContainerIdList) + .build(); + } +} diff --git a/src/main/java/carbonneutral/academy/api/converter/user/UserConverter.java b/src/main/java/carbonneutral/academy/api/converter/user/UserConverter.java new file mode 100644 index 0000000..6ade9b7 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/converter/user/UserConverter.java @@ -0,0 +1,46 @@ +package carbonneutral.academy.api.converter.user; + +import carbonneutral.academy.api.controller.auth.dto.response.PatchAdditionalInfoRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.GetMyPageRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyStatisticsRes; +import carbonneutral.academy.api.controller.user.dto.response.PatchInfoRes; +import carbonneutral.academy.domain.point.Point; +import carbonneutral.academy.domain.use_statistics.UseStatistics; +import carbonneutral.academy.domain.user.User; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserConverter { + + public static PatchAdditionalInfoRes toPatchAdditionalInfoRes(User user) { + return PatchAdditionalInfoRes.builder() + .id(user.getId()) + .isFinished(user.isFinished()) + .build(); + } + public static GetMyPageRes toGetMyPageRes(User user, List getDailyStatisticsResList, List monthlyStatisticsResList, + Point point, UseStatistics useStatistics) { + return GetMyPageRes.builder() + .nickname(user.getNickname()) + .gender(user.isGender()) + .currentPoint(point.getAccumulatedPoint() - point.getUtilizedPoint()) + .totalUseCount(useStatistics.getTotalUseCount()) + .totalReturnCount(useStatistics.getTotalReturnCount()) + .dailyStatisticsResList(getDailyStatisticsResList) + .monthlyStatisticsResList(monthlyStatisticsResList) + .build(); + } + + public static PatchInfoRes toPatchInfoRes(User user) { + return PatchInfoRes.builder() + .editNickname(user.getNickname()) + .build(); + } + + + +} diff --git a/src/main/java/carbonneutral/academy/api/service/auth/AuthService.java b/src/main/java/carbonneutral/academy/api/service/auth/AuthService.java new file mode 100644 index 0000000..63e95f2 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/auth/AuthService.java @@ -0,0 +1,11 @@ +package carbonneutral.academy.api.service.auth; + +import carbonneutral.academy.api.controller.auth.dto.response.PostSocialRes; +import carbonneutral.academy.domain.user.enums.SocialType; + +public interface AuthService { + + PostSocialRes socialLogin(String code, SocialType socialType); + + +} diff --git a/src/main/java/carbonneutral/academy/api/service/auth/AuthServiceImpl.java b/src/main/java/carbonneutral/academy/api/service/auth/AuthServiceImpl.java new file mode 100644 index 0000000..f6b3d22 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/auth/AuthServiceImpl.java @@ -0,0 +1,93 @@ +package carbonneutral.academy.api.service.auth; + +import carbonneutral.academy.api.controller.auth.dto.response.GetKakaoRes; +import carbonneutral.academy.api.controller.auth.dto.response.PostSocialRes; +import carbonneutral.academy.api.converter.auth.AuthConverter; +import carbonneutral.academy.api.service.auth.social.kakao.KakaoLoginService; +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.domain.point.repository.PointJpaRepository; +import carbonneutral.academy.domain.use_statistics.repository.UseStatisticsJpaRepository; +import carbonneutral.academy.domain.user.User; +import carbonneutral.academy.domain.user.enums.SocialType; +import carbonneutral.academy.domain.user.repository.UserJpaRepository; +import carbonneutral.academy.utils.RedisProvider; +import carbonneutral.academy.utils.jwt.JwtProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static carbonneutral.academy.common.BaseEntity.State.ACTIVE; +import static carbonneutral.academy.common.code.status.ErrorStatus.INVALID_OAUTH_TYPE; +import static carbonneutral.academy.common.code.status.ErrorStatus.NOT_FIND_USER; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class AuthServiceImpl implements AuthService { + + private final UserJpaRepository userJpaRepository; + private final PointJpaRepository pointJpaRepository; + private final UseStatisticsJpaRepository useStatisticsJpaRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final JwtProvider jwtProvider; + private final RedisProvider redisProvider; + private final KakaoLoginService kakaoLoginService; + + + + @Override + @Transactional + public PostSocialRes socialLogin(String code, SocialType socialType) { + switch (socialType){ + case KAKAO: { + GetKakaoRes getKakaoRes = kakaoLoginService.getUserInfo(code); + + boolean isRegistered = userJpaRepository.existsByUsernameAndSocialTypeAndState(getKakaoRes.getId(), SocialType.KAKAO, ACTIVE); + + if (!isRegistered) { + User user = AuthConverter.toUser(getKakaoRes); + userJpaRepository.save(user); + pointJpaRepository.save(AuthConverter.toPoint(user)); + useStatisticsJpaRepository.save(AuthConverter.toUseStatistics(user)); + } + User user = userJpaRepository.findByUsernameAndState(getKakaoRes.getId(), ACTIVE) + .orElseThrow(() -> new BaseException(NOT_FIND_USER)); + String accessToken = jwtProvider.generateToken(user); + String refreshToken = jwtProvider.generateRefreshToken(user); + revokeAllUserTokens(user); + saveUserToken(user, refreshToken); + return AuthConverter.toPostSocialRes(user, accessToken, refreshToken); + } + case GOOGLE: { + //TODO 구글 로그인 + } + case NAVER: { + //TODO 네이버 로그인 + } + default:{ + throw new BaseException(INVALID_OAUTH_TYPE); + } + + } + } + + + + + private void saveUserToken(User user, String refreshToken) { + redisProvider.setValueOps(user.getUsername(), refreshToken); + redisProvider.expireValues(user.getUsername()); + } + + private void revokeAllUserTokens(User user) { + redisProvider.deleteValueOps(user.getUsername()); + } + + +} + diff --git a/src/main/java/carbonneutral/academy/api/service/auth/social/kakao/KakaoLoginService.java b/src/main/java/carbonneutral/academy/api/service/auth/social/kakao/KakaoLoginService.java new file mode 100644 index 0000000..29638bb --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/auth/social/kakao/KakaoLoginService.java @@ -0,0 +1,9 @@ +package carbonneutral.academy.api.service.auth.social.kakao; + +import carbonneutral.academy.api.controller.auth.dto.response.GetKakaoRes; + +public interface KakaoLoginService { + + String getAccessToken(String authorizationCode); + GetKakaoRes getUserInfo(String accessToken); +} diff --git a/src/main/java/carbonneutral/academy/api/service/auth/social/kakao/KakaoLoginServiceImpl.java b/src/main/java/carbonneutral/academy/api/service/auth/social/kakao/KakaoLoginServiceImpl.java new file mode 100644 index 0000000..afdd625 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/auth/social/kakao/KakaoLoginServiceImpl.java @@ -0,0 +1,111 @@ +package carbonneutral.academy.api.service.auth.social.kakao; + +import carbonneutral.academy.api.controller.auth.dto.response.GetKakaoRes; +import carbonneutral.academy.common.exceptions.BaseException; +import com.nimbusds.jose.shaded.gson.JsonObject; +import com.nimbusds.jose.shaded.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +import static carbonneutral.academy.common.code.status.ErrorStatus.KAKAO_TOKEN_RECEIVE_FAIL; +import static carbonneutral.academy.common.code.status.ErrorStatus.TOKEN_NOT_FOUND; + + +@Slf4j +@Service +public class KakaoLoginServiceImpl implements KakaoLoginService { + + + + @Value("${spring.security.oauth2.client.provider.kakao.token-uri}") + private String KAKAO_TOKEN_URL; + + @Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}") + private String KAKAO_USER_INFO_URL; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String REDIRECT_URL; + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String KAKAO_API_KEY; + + @Override + public String getAccessToken(String authorizationCode) { + RestTemplate restTemplate = new RestTemplate(); + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", "authorization_code"); + map.add("client_id", KAKAO_API_KEY); + map.add("redirect_uri", REDIRECT_URL); + map.add("code", authorizationCode); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + HttpEntity> requestEntity = new HttpEntity<>(map, headers); + Map response = null; + try { + // 카카오 서버 호출하여 값 받음 + response = restTemplate.postForObject(KAKAO_TOKEN_URL, requestEntity, HashMap.class); + log.info("response is {}", response); + if(response != null && response.containsKey("access_token")) { + return response.get("access_token").toString(); + } else { + // 토큰이 없는 경우 예외 처리 + throw new BaseException(TOKEN_NOT_FOUND); + } + } catch (RestClientException e) { + // RestClientException 예외 처리 + log.error("RestClientException 발생: {}", e.getMessage()); + throw new BaseException(KAKAO_TOKEN_RECEIVE_FAIL); + } + } + + + @Override + public GetKakaoRes getUserInfo(String accessToken){ + RestTemplate restTemplate = new RestTemplate(); + log.info("getUserInfo accessToken is {}", accessToken); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> requestEntity = new HttpEntity<>(headers); + + String response = restTemplate.postForObject(KAKAO_USER_INFO_URL, requestEntity, String.class); + JsonObject rootObject = JsonParser.parseString(response).getAsJsonObject(); + JsonObject accountObject = rootObject.getAsJsonObject("kakao_account"); + + log.info("response is {}", response); + + String nickname = ""; + String id = ""; + + if (accountObject.has("profile") && !accountObject.get("profile").isJsonNull()) { + JsonObject profileObject = accountObject.getAsJsonObject("profile"); + if (profileObject.has("nickname") && !profileObject.get("nickname").isJsonNull()) { + nickname = profileObject.get("nickname").getAsString(); + } + } + + if (rootObject.has("id") && !rootObject.get("id").isJsonNull()) { + id = rootObject.get("id").getAsString(); // 직접 rootObject에서 id 값을 가져옴 + } + log.info("nickname is {}", nickname); + log.info("id is {}", id); + return GetKakaoRes.builder() + .name(nickname) + .id(id) + .build(); + } + +} + diff --git a/src/main/java/carbonneutral/academy/api/service/ocr/OcrService/OcrService.java b/src/main/java/carbonneutral/academy/api/service/ocr/OcrService/OcrService.java new file mode 100644 index 0000000..0da4a84 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/ocr/OcrService/OcrService.java @@ -0,0 +1,10 @@ +package carbonneutral.academy.api.service.ocr.OcrService; + +import carbonneutral.academy.api.controller.ocr.dto.response.PostOcrRes; +import carbonneutral.academy.domain.user.User; +import org.springframework.web.multipart.MultipartFile; + +public interface OcrService { + + PostOcrRes ocrImage(User user, MultipartFile receipt); +} diff --git a/src/main/java/carbonneutral/academy/api/service/ocr/OcrService/OcrServiceImpl.java b/src/main/java/carbonneutral/academy/api/service/ocr/OcrService/OcrServiceImpl.java new file mode 100644 index 0000000..780f758 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/ocr/OcrService/OcrServiceImpl.java @@ -0,0 +1,85 @@ +package carbonneutral.academy.api.service.ocr.OcrService; + +import carbonneutral.academy.api.controller.ocr.dto.response.PostOcrRes; +import carbonneutral.academy.api.converter.time.TimeConverter; +import carbonneutral.academy.api.converter.use.UseConverter; +import carbonneutral.academy.api.service.use.UseService; +import carbonneutral.academy.api.service.user.UserService; +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.location.repository.LocationJpaRepository; +import carbonneutral.academy.domain.point.Point; +import carbonneutral.academy.domain.point.repository.PointJpaRepository; +import carbonneutral.academy.domain.use.Use; +import carbonneutral.academy.domain.use.enums.UseStatus; +import carbonneutral.academy.domain.use.repository.UseJpaRepository; +import carbonneutral.academy.domain.use_statistics.repository.UseStatisticsJpaRepository; +import carbonneutral.academy.domain.user.User; +import carbonneutral.academy.utils.clova.ClovaOCR; +import carbonneutral.academy.utils.s3.S3Provider; +import carbonneutral.academy.utils.s3.dto.S3UploadRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; + +import static carbonneutral.academy.common.BaseEntity.State.ACTIVE; +import static carbonneutral.academy.common.code.status.ErrorStatus.*; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional +public class OcrServiceImpl implements OcrService { + + private final S3Provider s3Provider; + private final ClovaOCR clovaOCR; + private final UseJpaRepository useJpaRepository; + private final LocationJpaRepository locationJpaRepository; + private final UseStatisticsJpaRepository useStatisticsJpaRepository; + private final PointJpaRepository pointJpaRepository; + + @Override + public PostOcrRes ocrImage(User user, MultipartFile receipt) { + String receiptUrl = s3Provider.multipartFileUpload(receipt, S3UploadRequest.builder().userId(user.getId()).dirName("receipt").build()); + log.info("receiptUrl : {}", receiptUrl); + String result = clovaOCR.OCRParse(receiptUrl); + //결과에 컵 할인 없으면 예외 던지기 + if (!result.contains("개인컵") || !result.contains("컵 할인")) { + throw new BaseException(NOT_FIND_CUP_DISCOUNT); + } + Location location = locationJpaRepository.findByIdAndState(3, ACTIVE) + .orElseThrow(() -> new BaseException(NOT_FIND_LOCATION)); + Use use = Use.builder() + .useAt(LocalDateTime.now()) + .point(100) + .returnTime(LocalDateTime.now()) + .multiUseContainerId(5) + .rentalLocation(location) + .returnLocation(location) + .status(UseStatus.RETURNED) + .user(user) + .build(); + useJpaRepository.save(use); + Point point = pointJpaRepository.findByUserId(user.getId()).orElseThrow(() -> new BaseException(NOT_FIND_POINT)); + point.addPoint(100); + useStatisticsJpaRepository.findById(user.getId()).orElseThrow(() -> new BaseException(NOT_FIND_USE_STATISTICS)).addTotalUseCount(); + useStatisticsJpaRepository.findById(user.getId()).orElseThrow(() -> new BaseException(NOT_FIND_USE_STATISTICS)).addTotalReturnCount(); + return PostOcrRes.builder() + .useTime(TimeConverter.toFormattedDate(use.getUseAt())) + .currentPoint(point.getAccumulatedPoint() - point.getUtilizedPoint()) + .acquiredPoint(use.getPoint()) + .useLocationId(location.getId()) + .useLocationName(location.getName()) + .useLocationAddress(location.getAddress()) + .useLocationImageUrl(location.getImageUrl()) + .userId(user.getId()) + .build(); + } + + + +} diff --git a/src/main/java/carbonneutral/academy/api/service/point/PointService.java b/src/main/java/carbonneutral/academy/api/service/point/PointService.java new file mode 100644 index 0000000..c67e278 --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/point/PointService.java @@ -0,0 +1,9 @@ +package carbonneutral.academy.api.service.point; + +import carbonneutral.academy.api.controller.point.dto.response.GetBasePointRes; +import carbonneutral.academy.domain.user.User; +import org.springframework.data.domain.Slice; + +public interface PointService { + Slice getPoints(User user, Integer page, Integer size); +} diff --git a/src/main/java/carbonneutral/academy/api/service/point/PointServiceImpl.java b/src/main/java/carbonneutral/academy/api/service/point/PointServiceImpl.java new file mode 100644 index 0000000..e01956f --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/point/PointServiceImpl.java @@ -0,0 +1,53 @@ +package carbonneutral.academy.api.service.point; + +import carbonneutral.academy.api.controller.point.dto.response.GetAccumulatePointRes; +import carbonneutral.academy.api.controller.point.dto.response.GetBasePointRes; +import carbonneutral.academy.api.converter.time.TimeConverter; +import carbonneutral.academy.common.code.status.ErrorStatus; +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.domain.multi_use_container.MultiUseContainer; +import carbonneutral.academy.domain.multi_use_container.repository.MultiUseContainerJpaRepository; +import carbonneutral.academy.domain.use.Use; +import carbonneutral.academy.domain.use.repository.UseJpaRepository; +import carbonneutral.academy.domain.user.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static carbonneutral.academy.domain.use.enums.UseStatus.RETURNED; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class PointServiceImpl implements PointService { + private final UseJpaRepository useJpaRepository; + private final MultiUseContainerJpaRepository multiUseContainerJpaRepository; + @Override + public Slice getPoints(User user, Integer page, Integer size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Order.desc("useAt"))); + Slice uses = useJpaRepository.findByUserIdAndStatus(user.getId(), RETURNED, pageable); + List getBasePointResList = uses.getContent().stream().map(use -> { + MultiUseContainer multiUseContainer = multiUseContainerJpaRepository.findById(use.getMultiUseContainerId()) + .orElseThrow(() -> new BaseException(ErrorStatus.NOT_FIND_MULTI_USE_CONTAINER)); + return (GetBasePointRes)GetAccumulatePointRes.builder() + .rentalLocationName(use.getRentalLocation().getName()) + .rentalLocationAddress(use.getRentalLocation().getAddress()) + .returnLocationName(use.getReturnLocation().getName()) + .returnLocationAddress(use.getReturnLocation().getAddress()) + .useAt(TimeConverter.toFormattedDate(use.getUseAt())) + .useAtParsed(TimeConverter.toMonthDayString(use.getUseAt())) + .point(use.getPoint()) + .pointTypeImageUrl(multiUseContainer.getImageUrl()) + .build(); + }).toList(); + return new SliceImpl<>(getBasePointResList, pageable, uses.hasNext()); + } + + + +} diff --git a/src/main/java/carbonneutral/academy/api/service/use/UseService.java b/src/main/java/carbonneutral/academy/api/service/use/UseService.java new file mode 100644 index 0000000..acc7b0c --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/use/UseService.java @@ -0,0 +1,19 @@ +package carbonneutral.academy.api.service.use; + +import carbonneutral.academy.api.controller.use.dto.request.PatchReturnReq; +import carbonneutral.academy.api.controller.use.dto.request.PostUseReq; +import carbonneutral.academy.api.controller.use.dto.response.*; +import carbonneutral.academy.domain.user.User; + + +public interface UseService { + + GetHomeRes getInUsesMultipleTimeContainers(User user); + PostUseRes useMultipleTimeContainers(User user, PostUseReq postUseReq); + + GetUseDetailRes getInUseMultipleTimeContainer(User user, String useAt); + + PatchReturnRes returnMultipleTimeContainers(User user, PatchReturnReq patchReturnReq, String useAt); + + GetLocationRes getLocation(int locationId, int point); +} diff --git a/src/main/java/carbonneutral/academy/api/service/use/UseServiceImpl.java b/src/main/java/carbonneutral/academy/api/service/use/UseServiceImpl.java new file mode 100644 index 0000000..249531f --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/use/UseServiceImpl.java @@ -0,0 +1,145 @@ +package carbonneutral.academy.api.service.use; + +import carbonneutral.academy.api.controller.use.dto.request.PatchReturnReq; +import carbonneutral.academy.api.controller.use.dto.request.PostUseReq; +import carbonneutral.academy.api.controller.use.dto.response.*; +import carbonneutral.academy.api.converter.time.TimeConverter; +import carbonneutral.academy.api.converter.use.UseConverter; +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.location.enums.LocationType; +import carbonneutral.academy.domain.location.repository.LocationJpaRepository; +import carbonneutral.academy.domain.mapping.LocationContainer; +import carbonneutral.academy.domain.mapping.repository.LocationContainerJpaRepository; +import carbonneutral.academy.domain.multi_use_container.MultiUseContainer; +import carbonneutral.academy.domain.multi_use_container.repository.MultiUseContainerJpaRepository; +import carbonneutral.academy.domain.point.Point; +import carbonneutral.academy.domain.point.repository.PointJpaRepository; +import carbonneutral.academy.domain.use.Use; +import carbonneutral.academy.domain.use.repository.UseJpaRepository; +import carbonneutral.academy.domain.use_statistics.repository.UseStatisticsJpaRepository; +import carbonneutral.academy.domain.user.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Stream; + +import static carbonneutral.academy.common.BaseEntity.State.*; +import static carbonneutral.academy.common.code.status.ErrorStatus.*; +import static carbonneutral.academy.domain.use.enums.UseStatus.*; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class UseServiceImpl implements UseService { + + private final UseJpaRepository useJpaRepository; + private final LocationJpaRepository locationJpaRepository; + private final MultiUseContainerJpaRepository multiUseContainerJpaRepository; + private final LocationContainerJpaRepository locationContainerJpaRepository; + private final PointJpaRepository pointJpaRepository; + private final UseStatisticsJpaRepository useStatisticsJpaRepository; + + @Override + public GetHomeRes getInUsesMultipleTimeContainers(User user) { + List useResList = useJpaRepository.findByUserIdAndStatusOrderByUseAtDesc(user.getId(), USING).stream() + .map(use -> { + Location location = locationJpaRepository.findById(use.getRentalLocation().getId()).orElseThrow(() -> new BaseException(NOT_FIND_LOCATION)); + MultiUseContainer multiUseContainer = multiUseContainerJpaRepository.findById(use.getMultiUseContainerId()).orElseThrow(() -> new BaseException(NOT_FIND_MULTI_USE_CONTAINER)); + return UseConverter.toGetUseRes(use, location, multiUseContainer);}) + .toList(); + Point point = pointJpaRepository.findByUserId(user.getId()).orElseThrow(() -> new BaseException(NOT_FIND_POINT)); + return UseConverter.toGetHomesRes(user, point, useResList); + } + + @Override + public GetUseDetailRes getInUseMultipleTimeContainer(User user, String useAt) { + List localDateTime = TimeConverter.toLocalDateTime(useAt); + Use use = useJpaRepository.findByUserIdAndUseAtBetweenAndStatus(user.getId(), localDateTime.get(0), localDateTime.get(1), USING) + .orElseThrow(() -> new BaseException(NOT_FIND_USE)); + MultiUseContainer multiUseContainer = multiUseContainerJpaRepository.findById(use.getMultiUseContainerId()) + .orElseThrow(() -> new BaseException(NOT_FIND_MULTI_USE_CONTAINER)); + Location location = locationJpaRepository.findById(use.getRentalLocation().getId()) + .orElseThrow(() -> new BaseException(NOT_FIND_LOCATION)); + List locationIds = locationContainerJpaRepository.findByMultiUseContainerId(use.getMultiUseContainerId()) + .stream() + .map(LocationContainer::getLocationId) + .toList(); + List returnResList1 = locationJpaRepository.findByIdInAndLocationType(locationIds, LocationType.RETURN) + .stream() + .map(UseConverter::toGetReturnRes) + .toList(); + List returnResList2 = locationJpaRepository.findByIdInAndIsReturnedAndLocationType(locationIds, true, location.getLocationType()) + .stream() + .map(UseConverter::toGetReturnRes) + .toList(); + List returnResList = Stream.concat(returnResList1.stream(), returnResList2.stream()) + .toList(); + return UseConverter.toGetUseDetailRes(use, location, returnResList, multiUseContainer.getId()); + } + + @Override + public GetLocationRes getLocation(int locationId, int point) { + Location location = locationJpaRepository.findById(locationId).orElseThrow(() -> new BaseException(NOT_FIND_LOCATION)); + if(location.getLocationType().equals(LocationType.RETURN)) { + throw new BaseException(NOT_USE_LOCATION); + } + List multiUseContainerIdList = locationContainerJpaRepository.findByLocation_Id(location.getId()) + .stream() + .map(LocationContainer::getMultiUseContainer) + .map(MultiUseContainer::getId) + .toList(); + return UseConverter.toGetLocationRes(location, multiUseContainerIdList,point); + } + @Override + @Transactional + public PostUseRes useMultipleTimeContainers(User user, PostUseReq postUseReq) { + Location location = locationJpaRepository.findByIdAndState(postUseReq.getLocationId(), ACTIVE) + .orElseThrow(() -> new BaseException(NOT_FIND_LOCATION)); + List multiUseContainerIdList = locationContainerJpaRepository.findByLocation_Id(location.getId()) + .stream() + .map(LocationContainer::getMultiUseContainer) + .map(MultiUseContainer::getId) + .toList(); + if(!multiUseContainerIdList.contains(postUseReq.getMultiUseContainerId())) { + throw new BaseException(NOT_USE_LOCATION); + } + Use use = UseConverter.toUse(user, location, postUseReq.getPoint(), postUseReq.getMultiUseContainerId()); + useJpaRepository.save(use); + useStatisticsJpaRepository.findById(user.getId()).orElseThrow(() -> new BaseException(NOT_FIND_USE_STATISTICS)).addTotalUseCount(); + return UseConverter.toPostUseRes(use, location); + } + + @Override + @Transactional + public PatchReturnRes returnMultipleTimeContainers(User user, PatchReturnReq patchReturnReq, String usetAt) { + List localDateTime = TimeConverter.toLocalDateTime(usetAt); + Use use = useJpaRepository.findByUserIdAndUseAtBetweenAndStatus(user.getId(), localDateTime.get(0), localDateTime.get(1), USING) + .orElseThrow(() -> new BaseException(NOT_FIND_USE)); + log.info("use : {}", use.getUseAt()); + Location returnLocation = locationJpaRepository.findByIdAndState(patchReturnReq.getReturnLocationId(), ACTIVE) + .orElseThrow(() -> new BaseException(NOT_FIND_LOCATION)); + if(!(returnLocation.isReturned()) && !(returnLocation.getLocationType().equals(LocationType.RETURN))) { + throw new BaseException(NOT_RETURN_LOCATION); + } + if(!locationContainerJpaRepository.findByLocation_Id(returnLocation.getId()) + .stream() + .map(locationContainer -> locationContainer.getMultiUseContainer().getId()) + .toList().contains(use.getMultiUseContainerId())) { + throw new BaseException(NOT_RETURN_LOCATION); + } + use.setReturnLocation(returnLocation); + Point userPoint = pointJpaRepository.findByUserId(user.getId()) + .orElseThrow(() -> new BaseException(NOT_FIND_POINT)); + userPoint.addPoint(use.getPoint()); + useStatisticsJpaRepository.findById(user.getId()) + .orElseThrow(() -> new BaseException(NOT_FIND_USE_STATISTICS)) + .addTotalReturnCount(); + return UseConverter.toPatchReturnRes(user, returnLocation, use, userPoint); + } +} diff --git a/src/main/java/carbonneutral/academy/api/service/user/UserService.java b/src/main/java/carbonneutral/academy/api/service/user/UserService.java new file mode 100644 index 0000000..9a5b92f --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/user/UserService.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.api.service.user; + +import carbonneutral.academy.api.controller.auth.dto.request.PatchAdditionalInfoReq; +import carbonneutral.academy.api.controller.auth.dto.response.PatchAdditionalInfoRes; +import carbonneutral.academy.api.controller.use.dto.response.GetMyPageRes; +import carbonneutral.academy.api.controller.user.dto.request.PatchInfoReq; +import carbonneutral.academy.api.controller.user.dto.response.PatchInfoRes; +import carbonneutral.academy.domain.user.User; + +public interface UserService { + + PatchAdditionalInfoRes additionalInfo(User user, PatchAdditionalInfoReq request); + + GetMyPageRes mypage(User user); + + PatchInfoRes mypageEdit(User user, PatchInfoReq request); +} diff --git a/src/main/java/carbonneutral/academy/api/service/user/UserServiceImpl.java b/src/main/java/carbonneutral/academy/api/service/user/UserServiceImpl.java new file mode 100644 index 0000000..adeb62d --- /dev/null +++ b/src/main/java/carbonneutral/academy/api/service/user/UserServiceImpl.java @@ -0,0 +1,133 @@ +package carbonneutral.academy.api.service.user; + +import carbonneutral.academy.api.controller.auth.dto.request.PatchAdditionalInfoReq; +import carbonneutral.academy.api.controller.auth.dto.response.PatchAdditionalInfoRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyReturnStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyUseStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.GetMyPageRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyReturnStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyUseStatisticsRes; +import carbonneutral.academy.api.controller.user.dto.request.PatchInfoReq; +import carbonneutral.academy.api.controller.user.dto.response.PatchInfoRes; +import carbonneutral.academy.api.converter.user.UserConverter; +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.domain.point.Point; +import carbonneutral.academy.domain.point.repository.PointJpaRepository; +import carbonneutral.academy.domain.use.repository.UseQueryRepository; +import carbonneutral.academy.domain.use_statistics.UseStatistics; +import carbonneutral.academy.domain.use_statistics.repository.UseStatisticsJpaRepository; +import carbonneutral.academy.domain.user.User; +import carbonneutral.academy.domain.user.repository.UserJpaRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +import static carbonneutral.academy.common.code.status.ErrorStatus.NOT_FIND_POINT; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +public class UserServiceImpl implements UserService { + + private final UserJpaRepository userJpaRepository; + private final UseQueryRepository useQueryRepository; + private final PointJpaRepository pointJpaRepository; + private final UseStatisticsJpaRepository useStatisticsJpaRepository; + @Override + @Transactional + public PatchAdditionalInfoRes additionalInfo(User user, PatchAdditionalInfoReq request) { + user.updateAdditionalInfo(request); + User updateUser = userJpaRepository.save(user); + log.info("updateUser : {}", updateUser.getId()); + + return UserConverter.toPatchAdditionalInfoRes(updateUser); + } + + @Override + public GetMyPageRes mypage(User user) { + List dailyStatisticsResList = getDailyStatistics(user); + List monthlyStatisticsResList = getMonthlyStatistics(user); + Point point = pointJpaRepository.findByUserId(user.getId()) + .orElseThrow(() -> new BaseException(NOT_FIND_POINT)); + UseStatistics useStatistics = useStatisticsJpaRepository.findById(user.getId()) + .orElseThrow(() -> new BaseException(NOT_FIND_POINT)); + return UserConverter.toGetMyPageRes(user, dailyStatisticsResList, monthlyStatisticsResList, point, useStatistics); + } + + + @Override + @Transactional + public PatchInfoRes mypageEdit(User user, PatchInfoReq request) { + user.updateInfo(request); + User updateUser = userJpaRepository.save(user); + log.info("updateUser : {}", updateUser.getId()); + return UserConverter.toPatchInfoRes(updateUser); + } + + + private List getDailyStatistics(User user) { + List dailyUseStatistics = useQueryRepository.getDailyUseStatistics(user); + List dailyReturnStatistics = useQueryRepository.getDailyReturnStatistics(user); + Map statisticsMap = new HashMap<>(); + for (int dayOfWeek = 1; dayOfWeek <= 7; dayOfWeek++) { + statisticsMap.put(dayOfWeek, new GetDailyStatisticsRes(dayOfWeek, 0, 0)); + } + + for (GetDailyUseStatisticsRes useStat : dailyUseStatistics) { + int convertedDayOfWeek = convertDayOfWeek(useStat.getDayOfWeek()); + GetDailyStatisticsRes dailyStat = statisticsMap.get(convertedDayOfWeek); + dailyStat.setUseCount(dailyStat.getUseCount() + useStat.getUseCount()); + } + + for (GetDailyReturnStatisticsRes returnStat : dailyReturnStatistics) { + int convertedDayOfWeek = convertDayOfWeek(returnStat.getDayOfWeek()); + GetDailyStatisticsRes dailyStat = statisticsMap.get(convertedDayOfWeek); + dailyStat.setReturnCount(dailyStat.getReturnCount() + returnStat.getReturnCount()); + } + List dailyStatisticsRes = new ArrayList<>(statisticsMap.values()); + dailyStatisticsRes.sort(Comparator.comparingInt(GetDailyStatisticsRes::getDayOfWeek)); + + return dailyStatisticsRes; + } + + private int convertDayOfWeek(int dayOfWeek) { + if (dayOfWeek == Calendar.SUNDAY) { + return 7; + } else { + return dayOfWeek - 1; + } + } + + private List getMonthlyStatistics(User user) { + List monthlyUseStatistics = useQueryRepository.getMonthlyUseStatistics(user); + List monthlyReturnStatistics = useQueryRepository.getMonthlyReturnStatistics(user); + Map statisticsMap = new HashMap<>(); + for (int month = 1; month <= 12; month++) { + statisticsMap.put(month, new GetMonthlyStatisticsRes(month, 0, 0)); + } + + for (GetMonthlyUseStatisticsRes useStat : monthlyUseStatistics) { + GetMonthlyStatisticsRes monthlyStat = statisticsMap.get(useStat.getMonth()); + monthlyStat.setUseCount(monthlyStat.getUseCount() + useStat.getUseCount()); + } + + for (GetMonthlyReturnStatisticsRes returnStat : monthlyReturnStatistics) { + GetMonthlyStatisticsRes monthlyStat = statisticsMap.get(returnStat.getMonth()); + monthlyStat.setReturnCount(monthlyStat.getReturnCount() + returnStat.getReturnCount()); + } + List monthlyStatisticsRes = new ArrayList<>(statisticsMap.values()); + monthlyStatisticsRes.sort(Comparator.comparingInt(GetMonthlyStatisticsRes::getMonth)); + + return monthlyStatisticsRes; + } + + + + +} diff --git a/src/main/java/carbonneutral/academy/common/BaseEntity.java b/src/main/java/carbonneutral/academy/common/BaseEntity.java new file mode 100644 index 0000000..bfcf0d6 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/BaseEntity.java @@ -0,0 +1,35 @@ +package carbonneutral.academy.common; + +import jakarta.persistence.*; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public class BaseEntity { + + @CreatedDate + @Column(name = "createdAt", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updatedAt", nullable = false) + private LocalDateTime updatedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "state", nullable = false, length = 20) + protected State state = State.ACTIVE; + + public enum State { + ACTIVE, INACTIVE + } + + public void setState(State state) { + this.state = state; + } +} diff --git a/src/main/java/carbonneutral/academy/common/BaseResponse.java b/src/main/java/carbonneutral/academy/common/BaseResponse.java new file mode 100644 index 0000000..acd71be --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/BaseResponse.java @@ -0,0 +1,39 @@ +package carbonneutral.academy.common; + +import carbonneutral.academy.common.code.BaseCode; +import carbonneutral.academy.common.code.status.SuccessStatus; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class BaseResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + private final String code; + private final String message; + @JsonInclude(JsonInclude.Include.NON_NULL) + private T result; + + + // 성공한 경우 응답 생성 + + public static BaseResponse onSuccess(T result){ + return new BaseResponse<>(true, SuccessStatus.OK.getCode() , SuccessStatus.OK.getMessage(), result); + } + + public static BaseResponse of(BaseCode code, T result){ + return new BaseResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result); + } + + + // 실패한 경우 응답 생성 + public static BaseResponse onFailure(String code, String message, T data){ + return new BaseResponse<>(false, code, message, data); + } +} diff --git a/src/main/java/carbonneutral/academy/common/code/BaseCode.java b/src/main/java/carbonneutral/academy/common/code/BaseCode.java new file mode 100644 index 0000000..459012e --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/code/BaseCode.java @@ -0,0 +1,8 @@ +package carbonneutral.academy.common.code; + +public interface BaseCode { + + public ReasonDTO getReason(); + + public ReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/carbonneutral/academy/common/code/BaseErrorCode.java b/src/main/java/carbonneutral/academy/common/code/BaseErrorCode.java new file mode 100644 index 0000000..6248d7b --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/code/BaseErrorCode.java @@ -0,0 +1,8 @@ +package carbonneutral.academy.common.code; + +public interface BaseErrorCode { + + ErrorReasonDTO getReason(); + + ErrorReasonDTO getReasonHttpStatus(); +} diff --git a/src/main/java/carbonneutral/academy/common/code/ErrorReasonDTO.java b/src/main/java/carbonneutral/academy/common/code/ErrorReasonDTO.java new file mode 100644 index 0000000..fe99707 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/code/ErrorReasonDTO.java @@ -0,0 +1,19 @@ +package carbonneutral.academy.common.code; + +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +@Data // for equals method overriding +public class ErrorReasonDTO { + + private HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; + +} diff --git a/src/main/java/carbonneutral/academy/common/code/ReasonDTO.java b/src/main/java/carbonneutral/academy/common/code/ReasonDTO.java new file mode 100644 index 0000000..11c261a --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/code/ReasonDTO.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.common.code; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@Builder +public class ReasonDTO { + + private HttpStatus httpStatus; + + private final boolean isSuccess; + private final String code; + private final String message; + +} diff --git a/src/main/java/carbonneutral/academy/common/code/status/ErrorStatus.java b/src/main/java/carbonneutral/academy/common/code/status/ErrorStatus.java new file mode 100644 index 0000000..8ef224b --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/code/status/ErrorStatus.java @@ -0,0 +1,94 @@ +package carbonneutral.academy.common.code.status; + +import carbonneutral.academy.common.code.BaseErrorCode; +import carbonneutral.academy.common.code.ErrorReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + + /** + * 400 : Request, Response 오류 + */ + BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON4000", "잘못된 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON4001", "로그인 인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON4003", "금지된 요청입니다."), + RESPONSE_ERROR(HttpStatus.NOT_FOUND, "COMMON4004", "값을 불러오는데 실패하였습니다."), + + + USERS_EMPTY_EMAIL( HttpStatus.BAD_REQUEST, "USER4000", "이메일을 입력해주세요."), + POST_USERS_INVALID_EMAIL(HttpStatus.BAD_REQUEST,"USER4001" , "이메일 형식을 확인해주세요."), + POST_USERS_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "USER4002","중복된 아이디입니다."), + FAILED_TO_LOGIN(HttpStatus.BAD_REQUEST, "USER4003", "존재하지 않는 아이디거나 비밀번호가 틀렸습니다."), + NOT_FIND_USER(HttpStatus.NOT_FOUND, "USER4000", "일치하는 유저가 없습니다."), + EXIST_PHONE_NUMBER(HttpStatus.BAD_REQUEST, "USER4005", "이미 존재하는 전화번호입니다."), + INVALID_PHONE_NUMBER(HttpStatus.BAD_REQUEST, "USER4006", "핸드폰 번호 양식에 맞지 않습니다. 예시: +82-10-0000-0000"), + + NOT_FIND_LOCATION(HttpStatus.NOT_FOUND, "LOCATION4000", "존재하지 않는 위치입니다."), + NOT_RETURN_LOCATION(HttpStatus.BAD_REQUEST, "LOCATION4001", "반납할 수 있는 위치가 아닙니다."), + NOT_USE_LOCATION(HttpStatus.BAD_REQUEST, "LOCATION4002", "이용할 수 있는 위치가 아닙니다."), + //USE + NOT_FIND_USE(HttpStatus.NOT_FOUND, "USE4000", "존재하지 않는 이용내역입니다."), + + //MULTI_USE_CONTAINER + NOT_FIND_MULTI_USE_CONTAINER(HttpStatus.NOT_FOUND, "MULTI_USE_CONTAINER4000", "존재하지 않는 다회용기입니다."), + + NOT_FIND_POINT(HttpStatus.NOT_FOUND, "POINT4000", "존재하지 않는 포인트입니다."), + NOT_FIND_USE_STATISTICS(HttpStatus.NOT_FOUND, "USE_STATISTICS4000", "존재하지 않는 이용 통계입니다."), + EMPTY_JWT(HttpStatus.UNAUTHORIZED, "JWT4000", "JWT를 입력해주세요"), + INVALID_JWT(HttpStatus.UNAUTHORIZED, "JWT4001", "유효하지 않은 JWT입니다."), + INVALID_USER_JWT(HttpStatus.FORBIDDEN, "JWT4002", "권한이 없는 유저의 접근입니다."), + + KAKAO_TOKEN_RECEIVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "TOKEN4000", "카카오 서버로부터 액세스 토큰을 받는데 실패했습니다."), + TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "TOKEN4001", "토큰이 존재하지 않습니다."), + + INVALID_OAUTH_TYPE(HttpStatus.BAD_REQUEST, "OAUTH4000", "알 수 없는 소셜 로그인 형식입니다."), + + INVALID_PAGE(HttpStatus.BAD_REQUEST, "PAGE4000", "페이지는 0 이상이어야 합니다."), + //사이즈는 무조건 10 + INVALID_SIZE_10(HttpStatus.BAD_REQUEST, "PAGE4001", "사이즈는 10이어야 합니다."), + FILE_CONVERT(HttpStatus.BAD_REQUEST, "FILE4001", "파일 변환 실패."), + S3_UPLOAD(HttpStatus.BAD_REQUEST, "S3UPLOAD4001", "S3 파일 업로드 실패."), + NOT_FIND_CUP_DISCOUNT(HttpStatus.BAD_REQUEST, "CUP_DISCOUNT4000", "텀블러를 사용한 할인 내역이 없습니다."), + + + /** + * 500 : Database, Server 오류 + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5000", "서버 에러, 관리자에게 문의 바랍니다."), + DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5001", "데이터베이스 연결에 실패하였습니다."), + SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5002", "서버와의 연결에 실패하였습니다."), + PASSWORD_ENCRYPTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5003", "비밀번호 암호화에 실패하였습니다."), + PASSWORD_DECRYPTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5004", "비밀번호 복호화에 실패하였습니다"), + UNEXPECTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON5005", "예상치 못한 에러가 발생했습니다."); + + + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build() + ; + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/common/code/status/SuccessStatus.java b/src/main/java/carbonneutral/academy/common/code/status/SuccessStatus.java new file mode 100644 index 0000000..3cafd3a --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/code/status/SuccessStatus.java @@ -0,0 +1,55 @@ +package carbonneutral.academy.common.code.status; + +import carbonneutral.academy.common.code.BaseCode; +import carbonneutral.academy.common.code.ReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum SuccessStatus implements BaseCode { + + // 일반적인 응답 + OK(HttpStatus.OK, "COMMON2000", "성공입니다."), + + OAUTH_OK(HttpStatus.OK, "USER2003", "소셜 로그인 성공"), + //추가정보 입력 성공 + ADDITIONAL_INFO_OK(HttpStatus.OK, "USER2004", "추가정보 입력 성공"), + MYPAGE_OK(HttpStatus.OK, "USER2005", "마이페이지 조회 성공"), + MYPAGE_EDIT_OK(HttpStatus.OK, "USER2006", "마이페이지 정보 성공"), + + //이용성공 + USE_SAVE_OK(HttpStatus.CREATED, "USE2000", "이용 성공"), + //이용중인 다회용기 조회 성공 + IN_USES_OK(HttpStatus.OK, "USE2001", "이용중인 다회용기 조회 성공"), + IN_USE_OK(HttpStatus.OK, "USE2002", "이용중인 다회용기 단일 조회 성공" ), + RETURN_SAVE_OK(HttpStatus.CREATED, "USE2003", "반납 성공"), + GET_POINT_OK(HttpStatus.OK, "POINT2000", "포인트 조회 성공"), + LOCATION_OK(HttpStatus.OK, "LOCATION2000", "장소 조회 성공"), + OCR_OK(HttpStatus.OK, "OCR2000", "텀블러 이용 성공"); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReason() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .build(); + } + + @Override + public ReasonDTO getReasonHttpStatus() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .httpStatus(httpStatus) + .build() + ; + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/common/config/AppConfig.java b/src/main/java/carbonneutral/academy/common/config/AppConfig.java new file mode 100644 index 0000000..2f411a6 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/config/AppConfig.java @@ -0,0 +1,63 @@ +package carbonneutral.academy.common.config; + +import carbonneutral.academy.domain.user.repository.UserJpaRepository; +import carbonneutral.academy.utils.ApplicationAuditAware; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static carbonneutral.academy.common.BaseEntity.State.ACTIVE; + + +@Configuration +@RequiredArgsConstructor +public class AppConfig { + + private final UserJpaRepository userJpaRepository; + + @Bean + public UserDetailsService userDetailsService() { + return username -> userJpaRepository.findByUsernameAndState(username, ACTIVE) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuditorAware auditorAware() { + return new ApplicationAuditAware(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } + +} diff --git a/src/main/java/carbonneutral/academy/common/config/RedisConfig.java b/src/main/java/carbonneutral/academy/common/config/RedisConfig.java new file mode 100644 index 0000000..dce2a5f --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/config/RedisConfig.java @@ -0,0 +1,41 @@ +package carbonneutral.academy.common.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@RequiredArgsConstructor +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.data.redis.port}") + private int port; + @Value("${spring.data.redis.host}") + private String host; + + // RedisProperties로 yml에 저장한 host, post를 연결 + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} + diff --git a/src/main/java/carbonneutral/academy/common/config/S3Config.java b/src/main/java/carbonneutral/academy/common/config/S3Config.java new file mode 100644 index 0000000..a48b31e --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/config/S3Config.java @@ -0,0 +1,32 @@ +package carbonneutral.academy.common.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +public class S3Config { + @Value("${cloud.aws.region.static}") + private String region; + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .withRegion(region) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/common/config/SecurityConfig.java b/src/main/java/carbonneutral/academy/common/config/SecurityConfig.java new file mode 100644 index 0000000..18f2832 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/config/SecurityConfig.java @@ -0,0 +1,84 @@ +package carbonneutral.academy.common.config; + +import carbonneutral.academy.common.exceptions.handler.CustomAccessDeniedHandler; +import carbonneutral.academy.common.exceptions.handler.CustomAuthenticationEntryPoint; +import carbonneutral.academy.utils.jwt.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; + +import static carbonneutral.academy.domain.user.enums.Permission.*; +import static carbonneutral.academy.domain.user.enums.Role.ADMIN; +import static carbonneutral.academy.domain.user.enums.Role.MANAGER; +import static org.springframework.http.HttpMethod.*; +import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity +public class SecurityConfig { + private static final String URL = "/api/v1/management/**"; + private static final String[] WHITE_LIST_URL = { + "/", + "/api/v1/auth/**", + "/api/v1/test/**", + "/v2/api-docs", + "/v3/api-docs", + "/v3/api-docs/**", + "/swagger-resources/**", + "/configuration/ui", + "/configuration/security", + "/swagger-ui/**", + "/webjars/**", + "/swagger-ui.html", + "/error/**" + }; + private final JwtAuthenticationFilter jwtAuthFilter; + private final AuthenticationProvider authenticationProvider; + private final LogoutHandler logoutHandler; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(req -> + req.requestMatchers(WHITE_LIST_URL) + .permitAll() + .requestMatchers(URL).hasAnyRole(ADMIN.name(), MANAGER.name()) + .requestMatchers(GET, URL).hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name()) + .requestMatchers(POST, URL).hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name()) + .requestMatchers(PUT, URL).hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name()) + .requestMatchers(DELETE, URL).hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name()) + .anyRequest() + .authenticated() + ) + .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .authenticationProvider(authenticationProvider) + .logout(logout -> + logout.logoutUrl("/api/v1/auth/logout") + .addLogoutHandler(logoutHandler) + .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()) + ); + http.exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(customAuthenticationEntryPoint)); + http.exceptionHandling(exceptionHandling -> exceptionHandling + .accessDeniedHandler(customAccessDeniedHandler)); + return http.build(); + + } +} diff --git a/src/main/java/carbonneutral/academy/common/config/SwaggerConfig.java b/src/main/java/carbonneutral/academy/common/config/SwaggerConfig.java new file mode 100644 index 0000000..2762fc4 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/config/SwaggerConfig.java @@ -0,0 +1,46 @@ +package carbonneutral.academy.common.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class SwaggerConfig implements WebMvcConfigurer { + private static final String SECURITY_SCHEME_NAME = "authorization"; // 추가 + + @Value("${server.url}") + private String serverUrl; + + @Bean + public GroupedOpenApi jwtApi() { + return GroupedOpenApi.builder() + .group("jwt-api") + .pathsToMatch("/**") + .build(); + } + @Bean + public OpenAPI OpenApi() { + + return new OpenAPI() + .addServersItem(new Server().url(serverUrl)) + .components(new Components() + .addSecuritySchemes(SECURITY_SCHEME_NAME, new SecurityScheme() + .name(SECURITY_SCHEME_NAME) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))) + .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)) + .info(new Info() + .title("탄소중립 다회용기 서비스 API 명세서") + .description("탄소중립 다회용기 서비스 API 명세서") + .version("V1")); + } +} diff --git a/src/main/java/carbonneutral/academy/common/exceptions/BaseException.java b/src/main/java/carbonneutral/academy/common/exceptions/BaseException.java new file mode 100644 index 0000000..7f076c5 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/exceptions/BaseException.java @@ -0,0 +1,20 @@ +package carbonneutral.academy.common.exceptions; + +import carbonneutral.academy.common.code.BaseErrorCode; +import carbonneutral.academy.common.code.ErrorReasonDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BaseException extends RuntimeException { + private BaseErrorCode code; + + public ErrorReasonDTO getErrorReason() { + return this.code.getReason(); + } + + public ErrorReasonDTO getErrorReasonHttpStatus(){ + return this.code.getReasonHttpStatus(); + } +} diff --git a/src/main/java/carbonneutral/academy/common/exceptions/ExceptionAdvice.java b/src/main/java/carbonneutral/academy/common/exceptions/ExceptionAdvice.java new file mode 100644 index 0000000..57eb1f4 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/exceptions/ExceptionAdvice.java @@ -0,0 +1,127 @@ +package carbonneutral.academy.common.exceptions; + +import carbonneutral.academy.common.BaseResponse; +import carbonneutral.academy.common.code.ErrorReasonDTO; +import carbonneutral.academy.common.code.status.ErrorStatus; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}) +public class ExceptionAdvice extends ResponseEntityExceptionHandler { + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex, WebRequest request) { + String errorMessage = "접근 권한이 없습니다."; + String errorCode = "ACCESS_DENIED"; + BaseResponse response = BaseResponse.onFailure(errorCode, errorMessage, null); + return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); + } + @ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = e.getConstraintViolations().stream() + .map(constraintViolation -> constraintViolation.getMessage()) + .findFirst() + .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); + + return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request); + } + + + @Override + public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + Map errors = new LinkedHashMap<>(); + + ex.getBindingResult().getFieldErrors().stream() + .forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(ex, HttpHeaders.EMPTY,ErrorStatus.valueOf("BAD_REQUEST"),request,errors); + + } + + @ExceptionHandler + public ResponseEntity exception(Exception e, WebRequest request) { + e.printStackTrace(); + + return handleExceptionInternalFalse(e, ErrorStatus.INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus.INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage()); + } + + @ExceptionHandler(value = BaseException.class) + public ResponseEntity onThrowException(BaseException baseException, HttpServletRequest request) { + ErrorReasonDTO errorReasonHttpStatus = baseException.getErrorReasonHttpStatus(); + return handleExceptionInternal(baseException,errorReasonHttpStatus,null,request); + } + + + private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, + HttpHeaders headers, HttpServletRequest request) { + + BaseResponse body = BaseResponse.onFailure(reason.getCode(),reason.getMessage(),null); + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal( + e, + body, + headers, + reason.getHttpStatus(), + webRequest + ); + } + + private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { + BaseResponse body = BaseResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint); + return super.handleExceptionInternal( + e, + body, + headers, + status, + request + ); + } + + private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus, + WebRequest request, Map errorArgs) { + BaseResponse body = BaseResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } + + private ResponseEntity handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, WebRequest request) { + BaseResponse body = BaseResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/common/exceptions/handler/CustomAccessDeniedHandler.java b/src/main/java/carbonneutral/academy/common/exceptions/handler/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..4339155 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/exceptions/handler/CustomAccessDeniedHandler.java @@ -0,0 +1,35 @@ +package carbonneutral.academy.common.exceptions.handler; + +import carbonneutral.academy.common.code.ErrorReasonDTO; +import carbonneutral.academy.common.code.status.ErrorStatus; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.OutputStream; + +@Component +@Slf4j +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(ErrorStatus.FORBIDDEN.getHttpStatus().value()); + + ErrorReasonDTO errorReasonDTO = ErrorStatus.FORBIDDEN.getReasonHttpStatus(); + try (OutputStream os = response.getOutputStream()) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.writeValue(os, errorReasonDTO); + os.flush(); + } + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/common/exceptions/handler/CustomAuthenticationEntryPoint.java b/src/main/java/carbonneutral/academy/common/exceptions/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..e54404b --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/exceptions/handler/CustomAuthenticationEntryPoint.java @@ -0,0 +1,36 @@ +package carbonneutral.academy.common.exceptions.handler; + +import carbonneutral.academy.common.code.ErrorReasonDTO; +import carbonneutral.academy.common.code.status.ErrorStatus; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.OutputStream; + +@Component +@Slf4j +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authenticationException) throws IOException { + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(ErrorStatus.UNAUTHORIZED.getHttpStatus().value()); + + ErrorReasonDTO errorReasonDTO = ErrorStatus.UNAUTHORIZED.getReasonHttpStatus(); + try (OutputStream os = response.getOutputStream()) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.writeValue(os, errorReasonDTO); + os.flush(); + } + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/common/exceptions/handler/ExceptionHandler.java b/src/main/java/carbonneutral/academy/common/exceptions/handler/ExceptionHandler.java new file mode 100644 index 0000000..13561e5 --- /dev/null +++ b/src/main/java/carbonneutral/academy/common/exceptions/handler/ExceptionHandler.java @@ -0,0 +1,9 @@ +package carbonneutral.academy.common.exceptions.handler; + + +import carbonneutral.academy.common.code.BaseErrorCode; +import carbonneutral.academy.common.exceptions.BaseException; + +public class ExceptionHandler extends BaseException { + public ExceptionHandler(BaseErrorCode errorCode) {super(errorCode);} +} diff --git a/src/main/java/carbonneutral/academy/domain/location/Location.java b/src/main/java/carbonneutral/academy/domain/location/Location.java new file mode 100644 index 0000000..5f60d91 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/location/Location.java @@ -0,0 +1,46 @@ +package carbonneutral.academy.domain.location; + +import carbonneutral.academy.common.BaseEntity; +import carbonneutral.academy.domain.location.enums.LocationType; +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@Table(name = "location") +public class Location extends BaseEntity { + + @Id + @Column(name = "location_id", nullable = false, updatable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false, length = 20) + private String name; + + @Column(nullable = false, length = 50) + private String address; + + @Column(precision = 10, scale = 8, nullable = false) + private BigDecimal latitude; + + @Column(precision = 11, scale = 8, nullable = false) + private BigDecimal longitude; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20, name = "location_type") + private LocationType locationType; + + @Column(nullable = false, name = "is_returned") + private boolean isReturned = false; + + @Column(nullable = false, length = 200, name = "image_url") + private String imageUrl; + +} diff --git a/src/main/java/carbonneutral/academy/domain/location/enums/LocationType.java b/src/main/java/carbonneutral/academy/domain/location/enums/LocationType.java new file mode 100644 index 0000000..2c44850 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/location/enums/LocationType.java @@ -0,0 +1,18 @@ +package carbonneutral.academy.domain.location.enums; + +public enum LocationType { + CAFE("cafe"), + RESTAURANT("restaurant"), + RETURN("return"); + + + private final String description; + + LocationType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/carbonneutral/academy/domain/location/repository/LocationJpaRepository.java b/src/main/java/carbonneutral/academy/domain/location/repository/LocationJpaRepository.java new file mode 100644 index 0000000..d9cb80c --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/location/repository/LocationJpaRepository.java @@ -0,0 +1,20 @@ +package carbonneutral.academy.domain.location.repository; + +import carbonneutral.academy.common.BaseEntity; +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.location.enums.LocationType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface LocationJpaRepository extends JpaRepository{ + + Optional findByIdAndState(int locationId, BaseEntity.State state); + Optional findByNameAndAddressAndState (String name, String address, BaseEntity.State state); + + List findByIdInAndLocationType(List locationIds, LocationType locationType); + List findByIdInAndIsReturnedAndLocationType(List locationIds, boolean returned, LocationType locationType); + + +} diff --git a/src/main/java/carbonneutral/academy/domain/mapping/LocationContainer.java b/src/main/java/carbonneutral/academy/domain/mapping/LocationContainer.java new file mode 100644 index 0000000..33ee3f2 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/mapping/LocationContainer.java @@ -0,0 +1,32 @@ +package carbonneutral.academy.domain.mapping; + +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.multi_use_container.MultiUseContainer; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@IdClass(LocationContainerId.class) +@Table(name = "location_container") +public class LocationContainer { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "multi_use_container_id") + private MultiUseContainer multiUseContainer; + + //getLocationId + public int getLocationId() { + return location.getId(); + } +} diff --git a/src/main/java/carbonneutral/academy/domain/mapping/LocationContainerId.java b/src/main/java/carbonneutral/academy/domain/mapping/LocationContainerId.java new file mode 100644 index 0000000..d8cd9e8 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/mapping/LocationContainerId.java @@ -0,0 +1,10 @@ +package carbonneutral.academy.domain.mapping; + + +import java.io.Serializable; + +public class LocationContainerId implements Serializable { + + private int location; + private int multiUseContainer; +} diff --git a/src/main/java/carbonneutral/academy/domain/mapping/repository/LocationContainerJpaRepository.java b/src/main/java/carbonneutral/academy/domain/mapping/repository/LocationContainerJpaRepository.java new file mode 100644 index 0000000..2e3562c --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/mapping/repository/LocationContainerJpaRepository.java @@ -0,0 +1,15 @@ +package carbonneutral.academy.domain.mapping.repository; + +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.mapping.LocationContainer; +import carbonneutral.academy.domain.mapping.LocationContainerId; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LocationContainerJpaRepository extends JpaRepository { + + List findByMultiUseContainerId(int multiUseContainerId); + List findByLocation_Id(int locationId); + +} diff --git a/src/main/java/carbonneutral/academy/domain/multi_use_container/MultiUseContainer.java b/src/main/java/carbonneutral/academy/domain/multi_use_container/MultiUseContainer.java new file mode 100644 index 0000000..8a5099b --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/multi_use_container/MultiUseContainer.java @@ -0,0 +1,26 @@ +package carbonneutral.academy.domain.multi_use_container; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@Table(name = "multi_use_container") +public class MultiUseContainer { + + @Id + @Column(name = "multi_use_container_id", nullable = false, updatable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false, length = 20) + private String type; + + @Column(nullable = false, length = 200, name = "image_url") + private String imageUrl; + +} diff --git a/src/main/java/carbonneutral/academy/domain/multi_use_container/repository/MultiUseContainerJpaRepository.java b/src/main/java/carbonneutral/academy/domain/multi_use_container/repository/MultiUseContainerJpaRepository.java new file mode 100644 index 0000000..861665f --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/multi_use_container/repository/MultiUseContainerJpaRepository.java @@ -0,0 +1,7 @@ +package carbonneutral.academy.domain.multi_use_container.repository; + +import carbonneutral.academy.domain.multi_use_container.MultiUseContainer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MultiUseContainerJpaRepository extends JpaRepository { +} diff --git a/src/main/java/carbonneutral/academy/domain/point/Point.java b/src/main/java/carbonneutral/academy/domain/point/Point.java new file mode 100644 index 0000000..7cf60f3 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/point/Point.java @@ -0,0 +1,34 @@ +package carbonneutral.academy.domain.point; + +import carbonneutral.academy.domain.user.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@Table(name = "point") +public class Point { + + + @Id + @Column(name = "user_id", nullable = false, updatable = false) + private int id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false, name = "accumulated_point") + private int accumulatedPoint; + + @Column(nullable = false, name = "utilized_point") + private int utilizedPoint; + + public void addPoint(int point) { + this.accumulatedPoint += point; + } +} diff --git a/src/main/java/carbonneutral/academy/domain/point/repository/PointJpaRepository.java b/src/main/java/carbonneutral/academy/domain/point/repository/PointJpaRepository.java new file mode 100644 index 0000000..24f2a5c --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/point/repository/PointJpaRepository.java @@ -0,0 +1,10 @@ +package carbonneutral.academy.domain.point.repository; + +import carbonneutral.academy.domain.point.Point; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PointJpaRepository extends JpaRepository { + Optional findByUserId(int userId); +} diff --git a/src/main/java/carbonneutral/academy/domain/use/Use.java b/src/main/java/carbonneutral/academy/domain/use/Use.java new file mode 100644 index 0000000..0fbb6a4 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/use/Use.java @@ -0,0 +1,56 @@ +package carbonneutral.academy.domain.use; + +import carbonneutral.academy.domain.location.Location; +import carbonneutral.academy.domain.use.enums.UseStatus; +import carbonneutral.academy.domain.user.User; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@Table(name = "uses") +public class Use { + + @Id + @Column(nullable = false, updatable = false, name = "use_at") + private LocalDateTime useAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "rental_location_id") + private Location rentalLocation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "return_location_id") + private Location returnLocation; + + @Column(name = "return_time") + private LocalDateTime returnTime; + + @Column(nullable = false, name = "multi_use_container_id") + private int multiUseContainerId; + + @Column(nullable = false) + private int point; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UseStatus status; + + + public void setReturnLocation(Location returnLocation) { + this.returnLocation = returnLocation; + this.returnTime = LocalDateTime.now(); + this.status = UseStatus.RETURNED; + } +} diff --git a/src/main/java/carbonneutral/academy/domain/use/enums/UseStatus.java b/src/main/java/carbonneutral/academy/domain/use/enums/UseStatus.java new file mode 100644 index 0000000..2dcb68b --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/use/enums/UseStatus.java @@ -0,0 +1,19 @@ +package carbonneutral.academy.domain.use.enums; + +public enum UseStatus { + + USING("이용중"), + RETURNED("반납완료"), + CANCELED("이용취소"), + EXPIRED("기간지남"); + + private final String description; + + UseStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/carbonneutral/academy/domain/use/repository/UseJpaRepository.java b/src/main/java/carbonneutral/academy/domain/use/repository/UseJpaRepository.java new file mode 100644 index 0000000..44a04f6 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/use/repository/UseJpaRepository.java @@ -0,0 +1,22 @@ +package carbonneutral.academy.domain.use.repository; + +import carbonneutral.academy.domain.use.Use; +import carbonneutral.academy.domain.use.enums.UseStatus; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface UseJpaRepository extends JpaRepository { + //최신순 + List findByUserIdAndStatusOrderByUseAtDesc(int userId, UseStatus status); + Optional findByUserIdAndUseAtBetweenAndStatus(int userId, LocalDateTime startRange, LocalDateTime endRange, UseStatus status); + + Slice findByUserIdAndStatus(int userId, UseStatus status, Pageable pageable); + +} diff --git a/src/main/java/carbonneutral/academy/domain/use/repository/UseQueryRepository.java b/src/main/java/carbonneutral/academy/domain/use/repository/UseQueryRepository.java new file mode 100644 index 0000000..ca471ce --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/use/repository/UseQueryRepository.java @@ -0,0 +1,97 @@ +package carbonneutral.academy.domain.use.repository; + +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyReturnStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.GetDailyUseStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.QGetDailyReturnStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.daily.QGetDailyUseStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyReturnStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.GetMonthlyUseStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.QGetMonthlyReturnStatisticsRes; +import carbonneutral.academy.api.controller.use.dto.response.statistics.monthly.QGetMonthlyUseStatisticsRes; +import carbonneutral.academy.domain.use.enums.UseStatus; +import carbonneutral.academy.domain.user.User; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +import static carbonneutral.academy.domain.use.QUse.use; + +@Repository +@RequiredArgsConstructor +public class UseQueryRepository { + + private final JPAQueryFactory queryFactory; + + + public List getDailyUseStatistics(User user) { + LocalDateTime oneWeekAgo = LocalDateTime.now().minusDays(7); + return queryFactory + .select(new QGetDailyUseStatisticsRes( + use.useAt.dayOfWeek().intValue(), + use.status.when(UseStatus.USING).then(1) + .when(UseStatus.RETURNED).then(1) + .otherwise(0).sum() + )) + .from(use) + .where(use.user.eq(user) + .and(use.useAt.after(oneWeekAgo)) + .and(use.useAt.before(LocalDateTime.now()))) + .groupBy(use.useAt.dayOfWeek()) + .orderBy(use.useAt.dayOfWeek().asc()) + .fetch(); + } + public List getDailyReturnStatistics(User user) { + LocalDateTime oneWeekAgo = LocalDateTime.now().minusDays(7); + return queryFactory + .select(new QGetDailyReturnStatisticsRes( + use.returnTime.dayOfWeek().intValue(), + use.status.when(UseStatus.RETURNED).then(1).otherwise(0).sum() + )) + .from(use) + .where(use.user.eq(user) + .and(use.returnTime.after(oneWeekAgo)) + .and(use.returnTime.before(LocalDateTime.now()))) + .groupBy(use.returnTime.dayOfWeek()) + .orderBy(use.returnTime.dayOfWeek().asc()) + .fetch(); + } + + public List getMonthlyUseStatistics(User user) { + LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1); + return queryFactory + .select(new QGetMonthlyUseStatisticsRes( + use.useAt.month().intValue(), + use.status.when(UseStatus.USING).then(1) + .when(UseStatus.RETURNED).then(1) + .otherwise(0).sum() + )) + .from(use) + .where(use.user.eq(user) + .and(use.useAt.after(oneYearAgo)) + .and(use.useAt.before(LocalDateTime.now()))) + .groupBy(use.useAt.month()) + .orderBy(use.useAt.month().asc()) + .fetch(); + } + + public List getMonthlyReturnStatistics(User user) { + LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1); + return queryFactory + .select(new QGetMonthlyReturnStatisticsRes( + use.returnTime.month().intValue(), + use.status.when(UseStatus.RETURNED).then(1).otherwise(0).sum() + )) + .from(use) + .where(use.user.eq(user) + .and(use.returnTime.after(oneYearAgo)) + .and(use.returnTime.before(LocalDateTime.now()))) + .groupBy(use.returnTime.month()) + .orderBy(use.returnTime.month().asc()) + .fetch(); + } + +} + diff --git a/src/main/java/carbonneutral/academy/domain/use_statistics/UseStatistics.java b/src/main/java/carbonneutral/academy/domain/use_statistics/UseStatistics.java new file mode 100644 index 0000000..8437ee0 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/use_statistics/UseStatistics.java @@ -0,0 +1,38 @@ +package carbonneutral.academy.domain.use_statistics; + + +import carbonneutral.academy.domain.user.User; +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@Table(name = "use_statistics") +public class UseStatistics { + + @Id + @Column(name = "user_id", nullable = false, updatable = false) + private int id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + @Column(nullable = false, name = "total_use_count") + private int totalUseCount; + + @Column(nullable = false, name = "total_return_count") + private int totalReturnCount; + + + public void addTotalUseCount() { + this.totalUseCount++; + } + + public void addTotalReturnCount() { + this.totalReturnCount++; + } +} diff --git a/src/main/java/carbonneutral/academy/domain/use_statistics/repository/UseStatisticsJpaRepository.java b/src/main/java/carbonneutral/academy/domain/use_statistics/repository/UseStatisticsJpaRepository.java new file mode 100644 index 0000000..ac247cb --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/use_statistics/repository/UseStatisticsJpaRepository.java @@ -0,0 +1,7 @@ +package carbonneutral.academy.domain.use_statistics.repository; + +import carbonneutral.academy.domain.use_statistics.UseStatistics; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UseStatisticsJpaRepository extends JpaRepository { +} diff --git a/src/main/java/carbonneutral/academy/domain/user/User.java b/src/main/java/carbonneutral/academy/domain/user/User.java new file mode 100644 index 0000000..54003a3 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/user/User.java @@ -0,0 +1,99 @@ +package carbonneutral.academy.domain.user; + +import carbonneutral.academy.api.controller.auth.dto.request.PatchAdditionalInfoReq; +import carbonneutral.academy.api.controller.user.dto.request.PatchInfoReq; +import carbonneutral.academy.common.BaseEntity; +import carbonneutral.academy.domain.user.enums.Role; +import carbonneutral.academy.domain.user.enums.SocialType; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +@Getter +@Builder +@Entity +@Table(name = "users") +public class User extends BaseEntity implements UserDetails { + + @Id + @Column(name = "user_id", nullable = false, updatable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + + @Column(nullable = false, length = 20) + private String username; + + @Column(length = 100) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20, name = "social_type") + private SocialType socialType; + + + @Column(length = 20) + private String nickname; + + private boolean gender; + + @Column(nullable = false, name = "is_finished") + private boolean isFinished = false; + + + @Column(length = 10) + @Enumerated(EnumType.STRING) + private Role role; + + + + @Override + public String getUsername() { + return username; + } + + @Override + public Collection getAuthorities() { + return role.getAuthorities(); + } + @Override + public String getPassword() { + return password; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + public void updateAdditionalInfo(PatchAdditionalInfoReq request) { + this.nickname = request.getNickname(); + this.gender = request.isGender(); + this.isFinished = true; + } + + public void updateInfo(PatchInfoReq request) { + this.nickname = request.getEditNickname(); + } +} diff --git a/src/main/java/carbonneutral/academy/domain/user/enums/Permission.java b/src/main/java/carbonneutral/academy/domain/user/enums/Permission.java new file mode 100644 index 0000000..6b4b90a --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/user/enums/Permission.java @@ -0,0 +1,20 @@ +package carbonneutral.academy.domain.user.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum Permission { + + ADMIN_READ("admin:read"), + ADMIN_UPDATE("admin:update"), + ADMIN_CREATE("admin:create"), + ADMIN_DELETE("admin:delete"), + MANAGER_READ("management:read"), + MANAGER_UPDATE("management:update"), + MANAGER_CREATE("management:create"), + MANAGER_DELETE("management:delete"); + + @Getter + private final String permission; +} diff --git a/src/main/java/carbonneutral/academy/domain/user/enums/Role.java b/src/main/java/carbonneutral/academy/domain/user/enums/Role.java new file mode 100644 index 0000000..0bc2d78 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/user/enums/Role.java @@ -0,0 +1,52 @@ +package carbonneutral.academy.domain.user.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static carbonneutral.academy.domain.user.enums.Permission.*; + + +@RequiredArgsConstructor +@Getter +public enum Role { + + USER(Collections.emptySet()), + ADMIN( + Set.of( + ADMIN_READ, + ADMIN_UPDATE, + ADMIN_DELETE, + ADMIN_CREATE, + MANAGER_READ, + MANAGER_UPDATE, + MANAGER_DELETE, + MANAGER_CREATE + ) + ), + MANAGER( + Set.of( + MANAGER_READ, + MANAGER_UPDATE, + MANAGER_DELETE, + MANAGER_CREATE + ) + ); + + @Getter + private final Set permissions; + + public List getAuthorities() { + var authorities = getPermissions() + .stream() + .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) + .collect(Collectors.toList()); + authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name())); + return authorities; + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/domain/user/enums/SocialType.java b/src/main/java/carbonneutral/academy/domain/user/enums/SocialType.java new file mode 100644 index 0000000..3b17a3d --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/user/enums/SocialType.java @@ -0,0 +1,18 @@ +package carbonneutral.academy.domain.user.enums; + +public enum SocialType { + NONE("none"), + GOOGLE("google"), + KAKAO("kakao"), + NAVER("naver"); + + private final String description; + + SocialType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/carbonneutral/academy/domain/user/repository/UserJpaRepository.java b/src/main/java/carbonneutral/academy/domain/user/repository/UserJpaRepository.java new file mode 100644 index 0000000..e126aa0 --- /dev/null +++ b/src/main/java/carbonneutral/academy/domain/user/repository/UserJpaRepository.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.domain.user.repository; + +import carbonneutral.academy.domain.user.User; +import carbonneutral.academy.domain.user.enums.SocialType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +import static carbonneutral.academy.common.BaseEntity.*; + +public interface UserJpaRepository extends JpaRepository { + + Optional findByUsernameAndState(String username, State state); + + boolean existsByUsernameAndSocialTypeAndState(String username, SocialType socialType, State state); + +} diff --git a/src/main/java/carbonneutral/academy/utils/ApplicationAuditAware.java b/src/main/java/carbonneutral/academy/utils/ApplicationAuditAware.java new file mode 100644 index 0000000..0be61fb --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/ApplicationAuditAware.java @@ -0,0 +1,29 @@ +package carbonneutral.academy.utils; + +import carbonneutral.academy.domain.user.User; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Optional; + + +public class ApplicationAuditAware implements AuditorAware { + @Override + public Optional getCurrentAuditor() { + Authentication authentication = + SecurityContextHolder + .getContext() + .getAuthentication(); + if (authentication == null || + !authentication.isAuthenticated() || + authentication instanceof AnonymousAuthenticationToken + ) { + return Optional.empty(); + } + + User userPrincipal = (User) authentication.getPrincipal(); + return Optional.ofNullable(userPrincipal.getId()); + } +} diff --git a/src/main/java/carbonneutral/academy/utils/RedisProvider.java b/src/main/java/carbonneutral/academy/utils/RedisProvider.java new file mode 100644 index 0000000..ff96ff6 --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/RedisProvider.java @@ -0,0 +1,48 @@ +package carbonneutral.academy.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class RedisProvider { + private final RedisTemplate redisTemplate; + @Value("${jwt.refresh-token.expiration}") + private Long refreshExpiration; + + public void expireValues(String key) { + redisTemplate.expire(key, refreshExpiration, TimeUnit.MILLISECONDS); + } + + public void setValueOps(String key, String value) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value); + } + + @Transactional(readOnly = true) + public String getValueOps(String key) { + return (String) redisTemplate.opsForValue().get(key); + } + + public void deleteValueOps(String key) { + redisTemplate.delete(key); + } + + public void setDataExpire(String key,String value,long duration){ + ValueOperations valueOperations= redisTemplate.opsForValue(); + Duration expireDuration=Duration.ofSeconds(duration); + valueOperations.set(key,value,expireDuration); + } + + +} diff --git a/src/main/java/carbonneutral/academy/utils/clova/ClovaOCR.java b/src/main/java/carbonneutral/academy/utils/clova/ClovaOCR.java new file mode 100644 index 0000000..160ed19 --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/clova/ClovaOCR.java @@ -0,0 +1,132 @@ +package carbonneutral.academy.utils.clova; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@Component +public class ClovaOCR { + + @Value("${naver.service.url}") + private String API_URL; + + @Value("${naver.service.secretKey}") + private String SECRET_KEY; + + public String OCRParse(String imageUrl){ + try { + URL url = new URL(API_URL); + HttpURLConnection connection = createRequestHeader(url); + createRequestBody(connection, imageUrl); + + StringBuilder response = getResponse(connection); + + // 응답 로그 출력 + System.out.println("API Response: " + response.toString()); + + String result = parseResponseData(response.toString()); + + return result; + } catch(Exception e) { + e.printStackTrace(); + return null; + } + } + + private HttpURLConnection createRequestHeader(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setReadTimeout(5000); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json;"); + connection.setRequestProperty("X-OCR-SECRET", SECRET_KEY); + + return connection; + } + + private void createRequestBody(HttpURLConnection connection, String imageUrl) throws IOException { + JSONObject image = new JSONObject(); + + image.put("format", "png"); + image.put("name", "requestImage"); + image.put("url", imageUrl); + + JSONArray images = new JSONArray(); + images.put(image); + + JSONObject jsonRequest = new JSONObject(); + jsonRequest.put("version", "V1"); + jsonRequest.put("requestId", UUID.randomUUID().toString()); + jsonRequest.put("timestamp", System.currentTimeMillis()); + jsonRequest.put("lang", "ko"); + jsonRequest.put("resultType", "string"); + jsonRequest.put("images", images); + + connection.connect(); + DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream()); + dataOutputStream.write(jsonRequest.toString().getBytes(StandardCharsets.UTF_8)); + dataOutputStream.flush(); + dataOutputStream.close(); + } + + private BufferedReader checkResponse(HttpURLConnection connection) throws IOException { + int responseCode = connection.getResponseCode(); + BufferedReader bufferedReader; + + if (responseCode == 200) { + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + } else { + bufferedReader = new BufferedReader(new InputStreamReader(connection.getErrorStream())); + } + + return bufferedReader; + } + + private StringBuilder getResponse(HttpURLConnection connection) throws IOException { + BufferedReader bufferedReader = checkResponse(connection); + + String str; + StringBuilder response = new StringBuilder(); + + while ((str = bufferedReader.readLine()) != null) { + response.append(str); + } + bufferedReader.close(); + + return response; + } + + private String parseResponseData(String response) { + JSONObject jsonResponse = new JSONObject(response); + JSONArray parsedImages = jsonResponse.getJSONArray("images"); + StringBuilder result = new StringBuilder(); + + if (parsedImages != null && parsedImages.length() > 0) { + JSONObject parsedImage = parsedImages.getJSONObject(0); + JSONArray parsedText = parsedImage.getJSONArray("fields"); + + for (int i = 0; i < parsedText.length(); i++) { + JSONObject text = parsedText.getJSONObject(i); + result.append(text.getString("inferText")).append(" "); + } + } + + // 파싱된 결과 로그 출력 + System.out.println("Parsed Result: " + result.toString()); + + return result.toString(); + } +} diff --git a/src/main/java/carbonneutral/academy/utils/jwt/JwtAuthenticationFilter.java b/src/main/java/carbonneutral/academy/utils/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..d4e12f6 --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,73 @@ +package carbonneutral.academy.utils.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static carbonneutral.academy.utils.jwt.JwtProvider.HEADER_AUTHORIZATION; +import static carbonneutral.academy.utils.jwt.JwtProvider.TOKEN_PREFIX; + + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + private final UserDetailsService userDetailsService; + + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + ///api/v1/auth 는 권한 필요 없는 api 이므로 바로 통과 + if (request.getServletPath().contains("/api/v1/auth")) { + filterChain.doFilter(request, response); + return; + } + final String authHeader = request.getHeader(HEADER_AUTHORIZATION); + final String jwt; + final String username; + + // 헤더에 토큰이 없거나 Bearer 로 시작하지 않으면 통과 + if (authHeader == null ||!authHeader.startsWith(TOKEN_PREFIX)) { + filterChain.doFilter(request, response); + return; + } + jwt = authHeader.substring(7); + username = jwtProvider.extractUsername(jwt); + // 토큰이 유효하고 만료되지 않았다면 SecurityContext에 인증 정보를 저장 + // 토큰이 만료되지 않았는지는 JwtService에서 확인 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + if (jwtProvider.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/carbonneutral/academy/utils/jwt/JwtProvider.java b/src/main/java/carbonneutral/academy/utils/jwt/JwtProvider.java new file mode 100644 index 0000000..0510d4a --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/jwt/JwtProvider.java @@ -0,0 +1,136 @@ +package carbonneutral.academy.utils.jwt; + +import carbonneutral.academy.common.exceptions.BaseException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static carbonneutral.academy.common.code.status.ErrorStatus.INVALID_JWT; + + +@Service +@Slf4j +public class JwtProvider { + public final static String HEADER_AUTHORIZATION = "Authorization"; + public final static String TOKEN_PREFIX = "Bearer "; + + @Value("${jwt.secret_key}") + private String secretKey; + @Value("${jwt.expiration}") + private Long accessExpiration; + @Value("${jwt.refresh-token.expiration}") + private Long refreshExpiration; + @Value("${jwt.issuer}") + private String issuer; + + //JWT 토큰에서 subject를 추출하여 사용자 이름을 반환 + public String extractUsername(String token) { + try { + return extractClaim(token, Claims::getSubject); + } catch (io.jsonwebtoken.ExpiredJwtException | io.jsonwebtoken.MalformedJwtException | io.jsonwebtoken.security.SecurityException e) { + log.error("JWT 토큰이 유효하지 않습니다."); + throw new BaseException(INVALID_JWT); + } + } + + //토큰에서 특정 클레임을 추출 + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + //사용자 정보를 바탕으로 JWT 토큰을 생성 + public String generateToken(UserDetails userDetails) { + return generateToken(new HashMap<>(), userDetails); + } + + public String generateToken( + Map extraClaims, + UserDetails userDetails + ) { + return buildToken(extraClaims, userDetails, accessExpiration); + } + + //사용자 정보를 바탕으로 리프레시 토큰을 생성 + public String generateRefreshToken(UserDetails userDetails) { + return buildToken(new HashMap<>(), userDetails, refreshExpiration); + } + + // 주어진 클레임, 사용자 정보, 그리고 만료 시간을 바탕으로 JWT 토큰을 생성 + private String buildToken(Map extraClaims, UserDetails userDetails, long expiration) { + return Jwts + .builder() + .setClaims(extraClaims) + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer(issuer) + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + + + //토큰이 유효한지 확인 + public boolean isTokenValid(String token, UserDetails userDetails) { + try { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + } catch (io.jsonwebtoken.ExpiredJwtException | io.jsonwebtoken.MalformedJwtException | io.jsonwebtoken.security.SecurityException e) { + log.error("JWT 토큰이 유효하지 않습니다."); + throw new BaseException(INVALID_JWT); + } + } + + //토큰이 만료되었는지 확인 + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + //토큰에서 만료 시간을 추출 + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + //토큰에서 모든 클레임을 추출 + private Claims extractAllClaims(String token) { + return Jwts + .parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + //서명 키 반환, 토큰 생성하고 검증할 때 사용 + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + //토큰 발급 시간 + public Long getIssuedAt (String token) { + Claims claims = getClaims(token); + return claims.getIssuedAt().getTime(); + } + + private Claims getClaims(String token) { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/utils/jwt/LogoutService.java b/src/main/java/carbonneutral/academy/utils/jwt/LogoutService.java new file mode 100644 index 0000000..4082f6a --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/jwt/LogoutService.java @@ -0,0 +1,42 @@ +package carbonneutral.academy.utils.jwt; + +import carbonneutral.academy.utils.RedisProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Service; + +import static carbonneutral.academy.utils.jwt.JwtProvider.HEADER_AUTHORIZATION; +import static carbonneutral.academy.utils.jwt.JwtProvider.TOKEN_PREFIX; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class LogoutService implements LogoutHandler { + + private final JwtProvider jwtProvider; + private final RedisProvider redisProvider; + + @Override + public void logout( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) { + final String authHeader = request.getHeader(HEADER_AUTHORIZATION); + final String jwt; + if (authHeader == null ||!authHeader.startsWith(TOKEN_PREFIX)) { + return; + } + jwt = authHeader.substring(7); + String username = jwtProvider.extractUsername(jwt); + log.info("username: {}", username); + redisProvider.deleteValueOps(username); + SecurityContextHolder.clearContext(); + } +} diff --git a/src/main/java/carbonneutral/academy/utils/s3/S3Provider.java b/src/main/java/carbonneutral/academy/utils/s3/S3Provider.java new file mode 100644 index 0000000..3371e27 --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/s3/S3Provider.java @@ -0,0 +1,72 @@ +package carbonneutral.academy.utils.s3; + +import carbonneutral.academy.common.exceptions.BaseException; +import carbonneutral.academy.utils.s3.dto.S3UploadRequest; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.TransferManagerBuilder; +import com.amazonaws.services.s3.transfer.Upload; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +import static carbonneutral.academy.common.code.status.ErrorStatus.FILE_CONVERT; +import static carbonneutral.academy.common.code.status.ErrorStatus.S3_UPLOAD; + + +@Slf4j +@RequiredArgsConstructor +@Component +public class S3Provider { + private final AmazonS3 amazonS3Client; + private TransferManager transferManager; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @PostConstruct + public void init() { + this.transferManager = TransferManagerBuilder.standard() + .withS3Client(amazonS3Client) + .build(); + } + + @PreDestroy + public void shutdown() { + if (this.transferManager != null) { + this.transferManager.shutdownNow(); + } + } + + public String multipartFileUpload(MultipartFile file, S3UploadRequest request) { + String fileName = request.getUserId() + "/" + request.getDirName() + "/" + UUID.randomUUID() + "_" + file.getOriginalFilename(); + + try { + InputStream is = file.getInputStream(); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(file.getContentType()); + objectMetadata.setContentLength(file.getSize()); + Upload upload = transferManager.upload(bucket, fileName, is, objectMetadata); + try { + upload.waitForCompletion(); + } catch (InterruptedException e) { + log.error("S3 multipartFileUpload error", e); + throw new BaseException(S3_UPLOAD); + } + } catch (IOException e) { + log.error("File convert error", e); + throw new BaseException(FILE_CONVERT); + } + return amazonS3Client.getUrl(bucket, fileName).toString(); + } +} \ No newline at end of file diff --git a/src/main/java/carbonneutral/academy/utils/s3/dto/S3UploadRequest.java b/src/main/java/carbonneutral/academy/utils/s3/dto/S3UploadRequest.java new file mode 100644 index 0000000..bbb0e5a --- /dev/null +++ b/src/main/java/carbonneutral/academy/utils/s3/dto/S3UploadRequest.java @@ -0,0 +1,17 @@ +package carbonneutral.academy.utils.s3.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +@AllArgsConstructor +@Builder +public class S3UploadRequest { + + private int userId; + private String dirName; +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..b65ea69 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,56 @@ +spring: + config: + activate: + on-profile: dev + h2: + console.enabled: true + datasource: + url: jdbc:mysql://${DEV_DB_URL}:3306/swacademy_dev?useUnicode=yes&characterEncoding=UTF-8&autoReconnect=true&setTimezone=Asia/Seoul # 변경해주세요 + username: ${DEV_USERNAME} + password: ${DEV_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver # mysql 8버전 + jpa: + database-platform: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + generate-ddl: false + show-sql: true + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + data: + redis: + host: ${DEV_REDIS_HOST} + port: 6379 + security: + oauth2: + client: + registration: + kakao: + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-authentication-method: POST + authorization-grant-type: authorization_code + client-name: Kakao + provider: kakao + redirect-uri: ${DEV_REDIRECT} + scope: + - profile_nickname + - profile_image + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + +server: + url: https://dev.swacademy.store \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..7ced0c3 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,56 @@ +spring: + config: + activate: + on-profile: local + h2: + console.enabled: true + datasource: + url: jdbc:mysql://${LOCAL_DB_URL}?useUnicode=yes&characterEncoding=UTF-8&autoReconnect=true&setTimezone=Asia/Seoul # 변경해주세요 + username: ${LOCAL_USERNAME} + password: ${LOCAL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver # mysql 8버전 + jpa: + database-platform: org.hibernate.dialect.MySQLDialect + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + generate-ddl: false + show-sql: true + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + data: + redis: + host: 127.0.0.1 + port: 6379 + security: + oauth2: + client: + registration: + kakao: + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + client-authentication-method: POST + authorization-grant-type: authorization_code + client-name: Kakao + provider: kakao + redirect-uri: ${LOCAL_REDIRECT} + scope: + - profile_nickname + - profile_image + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + +server: + url: http://localhost:8080 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..6fb2648 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,35 @@ +spring: + jpa: + properties: + hibernate: + default_batch_fetch_size: 1000 + application: + name: academy + + +jwt: + issuer: academy@academy.com + secret_key: ${JWT_SECRET_KEY} + expiration: ${ACCESS_TOKEN_EXPIRATION} #86400000 + refresh-token: + expiration: ${REFRESH_TOKEN_EXPIRATION} #604800000 + + +naver: + service: + url: ${NAVER_SERVICE_URL} + secretKey: ${NAVER_SERVICE_SECRET_KEY} + + +cloud: + aws: + s3: + bucket: ${BUCKET_NAME} + stack: + auto: false + region: + static: ap-northeast-2 + credentials: + instance-profile: true + access-key: ${S3_ACCESS_KEY} + secret-key: ${S3_SECRET_KEY} diff --git a/src/test/java/carbonneutral/academy/AcademyApplicationTests.java b/src/test/java/carbonneutral/academy/AcademyApplicationTests.java new file mode 100644 index 0000000..02d1003 --- /dev/null +++ b/src/test/java/carbonneutral/academy/AcademyApplicationTests.java @@ -0,0 +1,13 @@ +package carbonneutral.academy; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AcademyApplicationTests { + + @Test + void contextLoads() { + } + +}