diff --git a/.github/pull_request_templete.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/pull_request_templete.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..4e45f799 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,64 @@ +name: CD with Gradle + +on: + push: + branches: [ "main", "develop" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'oracle' + + - name: Make env file + run: | + echo "${{ secrets.ENV }}" | base64 --decode > .env + + - name: Make docker-compose file + run: | + echo "\n" >> ./docker-compose.yml + echo "${{ secrets.DOCKER_COMPOSE }}" | base64 --decode >> ./docker-compose.yml + echo "\n" >> ./docker-compose.yml + + - name: Grant execute permission for gradlew + run: | + chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build -x test + + - name: Docker Login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Docker build & push to Docker repo + run: | + docker build -f Dockerfile -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }} . + docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }} + + - name: Deploy to server + uses: appleboy/ssh-action@master + id: deploy + with: + host: ${{ secrets.AWS_HOST }} + username: ubuntu + key: ${{ secrets.AWS_SSH_KEY }} + envs: GITHUB_SHA + script: | + sudo docker rm -f $(docker ps -qa) + sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }} + sudo docker-compose up -d --build + sudo docker image prune -f diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..711d0f02 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [ "main", "develop" ] + pull_request: + branches: [ "main", "develop" ] + +permissions: + contents: read + checks: write + pull-requests: write + +jobs: + build: + name: Build and test project + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'oracle' + + - name: Make env file + run: | + echo "${{ secrets.ENV }}" | base64 --decode > .env + + - name: Make docker-compose file + run: | + echo "\n" >> ./docker-compose.yml + echo "${{ secrets.DOCKER_COMPOSE }}" | base64 --decode >> ./docker-compose.yml + echo "\n" >> ./docker-compose.yml + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew build -x test + + - name: Publish result of unit test + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: "**/build/test-results/test/TEST-*.xml" + github_token: ${{ github.token }} + + - name: Publish failure of unit test + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + github_token: ${{ github.token }} diff --git a/.gitignore b/.gitignore index c2065bc2..2c0c9410 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,13 @@ out/ ### VS Code ### .vscode/ + +### ENV FILE ### +.env + +### DATABASE/DATA Directory ### +database/data/ + +### REDIS/DATA Directory ### +redis/data/ +/src/main/generated/com/haejwo/tripcometrue diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3094db35 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY . . +COPY .env .env +ARG JAR_FILE_PATH=build/libs/*.jar +COPY ${JAR_FILE_PATH} app.jar +ENV TZ=Asia/Seoul +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md index 5f526cd7..6e45af46 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# TripComeTrue_BE +### ๐Ÿ‘คํŒ€์› ์†Œ๊ฐœ + +| Backend | Backend |Backend| Backend | Backend | +|:----------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------:| +| ์ด์œ ์ƒ | ์ด์ฃผ์—ฐ | ๋ฐฑ์ธ๊ถŒ | ๊น€๋™๋ฏผ | ๋ฐ•์ค€๋ชจ | +| [์ด์œ ์ƒ](https://github.com/liyusang1) | [์ด์ฃผ์—ฐ](https://github.com/jo0oy) |[๋ฐฑ์ธ๊ถŒ](https://github.com/BackInGone)| [๊น€๋™๋ฏผ](https://github.com/meena2003) | [๋ฐ•์ค€๋ชจ](https://github.com/junmo95) | +| ๐Ÿ’กspring security
๐Ÿ’กํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ
๐Ÿ’กSNS ๋กœ๊ทธ์ธ (์นด์นด์˜ค,๋„ค์ด๋ฒ„,๊ตฌ๊ธ€)
๐Ÿ’ก์—ฌํ–‰ ํ›„๊ธฐ ์ž‘์„ฑ
๐Ÿ’ก์—ฌํ–‰ ๊ณ„ํš ์ž‘์„ฑ ๋ฐ ์กฐํšŒ | ๐Ÿ’กGithub Actions CI/CD, EC2, Docker
๐Ÿ’กํ™ˆ ๋ฉ”์ธ ํ”ผ๋“œ ๊ด€๋ จ api
๐Ÿ’กํ™ˆ ๊ฒ€์ƒ‰ ํ”ผ๋“œ ๊ด€๋ จ api
๐Ÿ’ก๋„์‹œ ์ƒ์„ธ ๊ด€๋ จ api
๐Ÿ’กํ™˜์œจ open api ์Šค์ผ€์ค„๋Ÿฌ ํ˜ธ์ถœ ๋ฐ ํ™˜์œจ ์ •๋ณด redis ์ €์žฅ
|๐Ÿ’ก๋งˆ์ด ํŽ˜์ด์ง€ ๊ด€๋ จ api
๐Ÿ’ก๋ณด๊ด€ ๊ธฐ๋Šฅ ๊ด€๋ จ api
๐Ÿ’ก์ข‹์•„์š” ๊ด€๋ จ api | ๐Ÿ’กS3 ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‚ญ์ œ ๊ด€๋ จ api
๐Ÿ’ก ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ๋ฐ ๋ฆฌ๋ทฐ๋Œ“๊ธ€ ๊ด€๋ จ api
๐Ÿ’ก์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋ฐ ๋ฆฌ๋ทฐ๋Œ“๊ธ€ ๊ด€๋ จ api
| ๐Ÿ’ก์—ฌํ–‰ ํ›„๊ธฐ ๋ฉ”์ธํŽ˜์ด์ง€ api
๐Ÿ’ก์—ฌํ–‰ ํ›„๊ธฐ ์ƒ์„ธํŽ˜์ด์ง€ api
๐Ÿ’ก์—ฌํ–‰์ง€ ์ƒ์„ธํŽ˜์ด์ง€ api | + +----------------------- +### ๐Ÿ“Œ๊ธฐ์ˆ ์Šคํƒ & ๊ตฌํ˜„ํ™˜๊ฒฝ +> - Java : ![Java](https://img.shields.io/badge/java-17-red.svg) +> - FrameWork : ![Spring Boot](https://img.shields.io/badge/springboot-3.2.1-brightgreen.svg) ![Spring Security](https://img.shields.io/badge/springsecurity-brightgreen.svg) ![Spring Data JPA](https://img.shields.io/badge/spring%20data%20JPA-brightgreen.svg) ![Spring Web](https://img.shields.io/badge/spring%20web-brightgreen.svg) +> - Build : ![Gradle](https://img.shields.io/badge/Build-Gradle-blue.svg) +> - VCS : ![Git](https://img.shields.io/badge/VCS-Git-orange.svg) ![GitHub](https://img.shields.io/badge/Github-black.svg) +> - Database : ![GCP Cloud SQL](https://img.shields.io/badge/Database-AmazonEC2-yellow.svg) +> - DBMS : ![MySQL](https://img.shields.io/badge/DBMS-MySQL-blue.svg) +> - ๋ฐฐํฌํ™˜๊ฒฝ : ![GCP VM](https://img.shields.io/badge/๋ฐฐํฌ%20ํ™˜๊ฒฝ-AmazonEc2-blue.svg) +> - ์ปจ๋ฒค์…˜ : ![Code Convention](https://img.shields.io/badge/Code%20Convention-IntelliJ%20Java%20Google%20Style-brightgreen.svg) +> - ๋ธŒ๋žœ์น˜ ์ „๋žต : ![GitFlow](https://img.shields.io/badge/GitFlow-Workflow-orange.svg) + +### ๐Ÿ“ŒAPI ๋ช…์„ธ์„œ & ์„œ๋น„์Šค ๋ฐฐํฌ ์ฃผ์†Œ +- ์„œ๋น„์Šค ๋ฐฐํฌ ์ฃผ์†Œ : [https://tripcometrue.vercel.app](https://tripcometrue.vercel.app) +- API ๋ช…์„ธ์„œ: + - [Postman 1](https://documenter.getpostman.com/view/14269013/2s9YsJCYY9#47909ddc-026a-4731-b0ca-5088b8e8574f) + - [Postman 2](https://documenter.getpostman.com/view/24478928/2s9YsRaUDD) + - [Notion 1](https://arrow-halibut-e8d.notion.site/API-9d3aa3736a764af6a513efda552211b5?pvs=4) + - [Notion 2](https://immense-soarer-ecc.notion.site/TripComeTrue-API-4f9c4ca0580e47e49adfe3b62ec39957?pvs=4) + + +### ๐Ÿ“ŒํŒจํ‚ค์ง€ ๊ตฌ์กฐ +``` + com.example.yanolja + โ”œโ”€โ”€ domain + โ”‚ โ”œโ”€โ”€ alarm + โ”‚ โ”œโ”€โ”€ city + โ”‚ โ”œโ”€โ”€ comment + โ”‚ โ”œโ”€โ”€ likes + โ”‚ โ”œโ”€โ”€ member + โ”‚ โ”œโ”€โ”€ place + โ”‚ โ”œโ”€โ”€ review + โ”‚ โ”œโ”€โ”€ store + โ”‚ โ”œโ”€โ”€ tripplan + โ”‚ โ”œโ”€โ”€ triprecord + โ”‚ ... + โ””โ”€โ”€ global + โ”œโ”€โ”€ config + โ”œโ”€โ”€ entity + โ”œโ”€โ”€ enums + โ”œโ”€โ”€ exception + โ”œโ”€โ”€ jwt + โ”œโ”€โ”€ s3 + โ”œโ”€โ”€ springsecurity + โ”œโ”€โ”€ util + โ””โ”€โ”€ validator +``` + +-------------------- + +### โญERD +![erd](https://github.com/TripComeTrue/TripComeTrue_BE/assets/65541248/33ff36cc-0e04-4285-82d4-b2939e77b5ab) + + +### โญProject Architecture +![image](https://github.com/TripComeTrue/TripComeTrue_BE/assets/65541248/a6a64b92-2d77-4240-a64e-fe6770e1703e) diff --git a/build.gradle b/build.gradle index 375ad6ab..7a6e3b9b 100644 --- a/build.gradle +++ b/build.gradle @@ -26,19 +26,80 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' compileOnly 'org.projectlombok:lombok' - //mysql connector + // mysql connector runtimeOnly 'com.mysql:mysql-connector-j' + // queryDSL ์„ค์ • + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // testcontainers + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.testcontainers:mysql' + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.projectlombok:lombok' // yml implementation 'org.yaml:snakeyaml:+' + + // dotenv-java + implementation 'io.github.cdimascio:java-dotenv:+' + + + // aws S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + //oauth + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + + // jjwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + + // httpclient5 + implementation 'org.apache.httpcomponents.client5:httpclient5:5.3' +} + +jar { + enabled = false } tasks.named('test') { useJUnitPlatform() } + + +// Querydsl ์„ค์ •๋ถ€ +// ์•„๋ž˜ ๊ฒƒ๋“ค์ด ์—†์–ด๋„ ๊ธฐ๋ณธ์ ์ธ querydsl ๋™์ž‘์€ ํ•˜๋‚˜ ์ธํ…Œ๋ฆฌ์ œ์ด์—์„œ ๋นŒ๋“œ ์‹œ ๋ฐœ์ƒํ•  ๋ฌธ์ œ๋ฅผ ์˜ˆ๋ฐฉ +def generated = 'src/main/generated' + +// querydsl QClass ํŒŒ์ผ ์ƒ์„ฑ ์œ„์น˜๋ฅผ ์ง€์ • +// ์›๋ž˜ build ๋””๋ ‰ํ† ๋ฆฌ ์•ˆ์— ์žˆ์–ด์„œ ๋ˆˆ์— ์•ˆ๋ณด์˜€์ง€๋งŒ ๊บผ๋„ค์„œ ๋‚ด๊ฐ€ ์ง€์ •ํ•œ ๋””๋ ‰ํ† ๋ฆฌ์— ๊บผ๋‚ด์˜ด +// ์ธํ…”๋ฆฌ์ œ์ด IDE์™€์˜ ๋ฌธ์ œ์ธ๋ฐ, ๋นŒ๋“œ gradle ํ• ๋•Œ ์Šค์บ” ์˜์—ญ์ด ๋‹ฌ๋ผ์„œ ์ค‘๋ณต ์Šค์บ”์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +// java source set ์— querydsl QClass ์œ„์น˜ ์ถ”๊ฐ€ +sourceSets { + main.java.srcDirs += [ generated ] +} + +// gradle clean ์‹œ์— QClass ๋””๋ ‰ํ† ๋ฆฌ ์‚ญ์ œ +clean { + delete file(generated) +} diff --git a/database/Dockerfile b/database/Dockerfile index 53bb0f93..f79306ab 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -4,8 +4,6 @@ MAINTAINER liyusang799@gmail.com COPY init.sql /docker-entrypoint-initdb.d -ENV MYSQL_ROOT_PASSWORD=root - VOLUME /var/lib/mysql EXPOSE 3307 \ No newline at end of file diff --git a/database/init.sql b/database/init.sql index a383a8ae..0e756bf7 100644 --- a/database/init.sql +++ b/database/init.sql @@ -1,6 +1,3 @@ -alter user 'root'@'localhost' identified with caching_sha2_password by 'root'; -flush privileges; - create database if not exists tripcometrue; -use tripcometrue; \ No newline at end of file +use tripcometrue; diff --git a/docker-compose.yml b/docker-compose.yml index 9223b645..9f732e2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,23 @@ version: '3' + +networks: + network: + services: - travel-db: + tripcometrue-db: container_name: tripcometrue-db - build: - context: ./database - dockerfile: Dockerfile + image: jo0oy/tripcometrue-mysql ports: - "3307:3306" + environment: + - MYSQL_DATABASE=tripcometrue + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - TZ=Asia/Seoul + volumes: + - ./database/data:/var/lib/mysql + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci restart: always healthcheck: test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] @@ -15,5 +26,20 @@ services: networks: - network -networks: - network: \ No newline at end of file + tripcometrue-redis: + container_name: tripcometrue-redis + image: redis:6 + hostname: redis + command: redis-server --port 6379 + ports: + - "6379:6379" + volumes: + - ./redis/data:/data + environment: + - TZ=Asia/Seoul + labels: + - "name=redis" + - "mode=standalone" + restart: always + networks: + - network diff --git a/src/main/java/com/haejwo/tripcometrue/domain/alarm/controller/AlarmController.java b/src/main/java/com/haejwo/tripcometrue/domain/alarm/controller/AlarmController.java new file mode 100644 index 00000000..f1737f19 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/alarm/controller/AlarmController.java @@ -0,0 +1,30 @@ +package com.haejwo.tripcometrue.domain.alarm.controller; + +import com.haejwo.tripcometrue.domain.alarm.dto.response.AlarmResponseDto; +import com.haejwo.tripcometrue.domain.alarm.service.AlarmService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class AlarmController { + + private final AlarmService alarmService; + + + @GetMapping("/v1/member/alarms") + public ResponseEntity>> getAlarms( + @AuthenticationPrincipal PrincipalDetails principalDetails, + Pageable pageable + ) { + Page alarmsResponse = alarmService.getAlarms(principalDetails, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(alarmsResponse)); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/alarm/dto/response/AlarmResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/alarm/dto/response/AlarmResponseDto.java new file mode 100644 index 00000000..8669ccf0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/alarm/dto/response/AlarmResponseDto.java @@ -0,0 +1,30 @@ +package com.haejwo.tripcometrue.domain.alarm.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.alarm.entity.Alarm; +import com.haejwo.tripcometrue.domain.alarm.entity.AlarmType; +import java.time.LocalDateTime; + +public record AlarmResponseDto( + Long alarmId, + String alarmArgs, + String fromMemberNickname, + Long windowId, + Long objectId, + AlarmType alarmType, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt +) { + + public static AlarmResponseDto fromEntity(Alarm alarm) { + return new AlarmResponseDto( + alarm.getId(), + alarm.getAlarmArgs(), + alarm.getFromMember().getMemberBase().getNickname(), + alarm.getWindowId(), + alarm.getObjectId(), + alarm.getAlarmType(), + alarm.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/alarm/entity/Alarm.java b/src/main/java/com/haejwo/tripcometrue/domain/alarm/entity/Alarm.java new file mode 100644 index 00000000..e677bce9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/alarm/entity/Alarm.java @@ -0,0 +1,62 @@ +package com.haejwo.tripcometrue.domain.alarm.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Alarm extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "alarm_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "from_member_id") + private Member fromMember; + + private Long windowId; + private Long objectId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "to_member_id") + private Member toMember; + + @Enumerated(EnumType.STRING) + private AlarmType alarmType; + + private String alarmArgs; + + + @Builder + public Alarm( + Member fromMember, + Member toMember, + AlarmType alarmType, + Long windowId, + Long objectId, + String alarmArgs) { + this.fromMember = fromMember; + this.toMember = toMember; + this.alarmType = alarmType; + this.windowId = windowId; + this.objectId = objectId; + this.alarmArgs = alarmArgs; + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/alarm/entity/AlarmType.java b/src/main/java/com/haejwo/tripcometrue/domain/alarm/entity/AlarmType.java new file mode 100644 index 00000000..5b4449fd --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/alarm/entity/AlarmType.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.alarm.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AlarmType { + + NEW_TRIP_RECORD_REVIEW("์ƒˆ ๋ฆฌ๋ทฐ ์•Œ๋ฆผ"), + NEW_TRIP_RECORD_COMMENT("์ƒˆ ์—ฌํ–‰ํ›„๊ธฐ ๋Œ“๊ธ€ ์•Œ๋ฆผ"), + NEW_PLACE_REVIEW_COMMENT("์ƒˆ ์—ฌํ–‰์ง€๋ฆฌ๋ทฐ ๋Œ“๊ธ€ ์•Œ๋ฆผ"), + REVIEW_WRITE_REQUEST("๋ฆฌ๋ทฐ ์ž‘์„ฑ ์š”์ฒญ ์•Œ๋ฆผ"); + + private final String message; +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/alarm/repository/AlarmRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/alarm/repository/AlarmRepository.java new file mode 100644 index 00000000..ae73ab49 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/alarm/repository/AlarmRepository.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.alarm.repository; + +import com.haejwo.tripcometrue.domain.alarm.entity.Alarm; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AlarmRepository extends JpaRepository { + + Page findAllByToMember(Member member, Pageable pageable); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/alarm/service/AlarmService.java b/src/main/java/com/haejwo/tripcometrue/domain/alarm/service/AlarmService.java new file mode 100644 index 00000000..7116dfbc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/alarm/service/AlarmService.java @@ -0,0 +1,57 @@ +package com.haejwo.tripcometrue.domain.alarm.service; + +import com.haejwo.tripcometrue.domain.alarm.dto.response.AlarmResponseDto; +import com.haejwo.tripcometrue.domain.alarm.entity.Alarm; +import com.haejwo.tripcometrue.domain.alarm.entity.AlarmType; +import com.haejwo.tripcometrue.domain.alarm.repository.AlarmRepository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +@Transactional +public class AlarmService { + + private final AlarmRepository alarmRepository; + + + public Page getAlarms(PrincipalDetails principalDetails, Pageable pageable) { + Page alarms = alarmRepository.findAllByToMember(principalDetails.getMember(), pageable); + return alarms.map(AlarmResponseDto::fromEntity); + } + + public void addAlarm(Member fromMember, Member toMember, AlarmType alarmType, Long windowId, + Long objectId) { + String alarmArgs = createAlarmArgs(fromMember.getMemberBase().getNickname(), alarmType); + Alarm alarm = Alarm.builder() + .fromMember(fromMember) + .toMember(toMember) + .alarmType(alarmType) + .windowId(windowId) + .objectId(objectId) + .alarmArgs(alarmArgs) + .build(); + alarmRepository.save(alarm); + } + + private String createAlarmArgs(String memberNickname, AlarmType alarmType) { + switch (alarmType) { + case NEW_TRIP_RECORD_REVIEW: + return memberNickname + "๋‹˜์ด ์—ฌํ–‰ ํ›„๊ธฐ์— ๋ฆฌ๋ทฐ๋ฅผ ๋‚จ๊ธฐ์…จ์Šต๋‹ˆ๋‹ค."; + case NEW_TRIP_RECORD_COMMENT: + return memberNickname + "๋‹˜์ด ์—ฌํ–‰ํ›„๊ธฐ์— ๋Œ“๊ธ€์„ ๋‚จ๊ธฐ์…จ์Šต๋‹ˆ๋‹ค."; + case NEW_PLACE_REVIEW_COMMENT: + return memberNickname + "๋‹˜์ด ์—ฌํ–‰์ง€๋ฆฌ๋ทฐ์— ๋Œ“๊ธ€์„ ๋‚จ๊ธฐ์…จ์Šต๋‹ˆ๋‹ค."; + case REVIEW_WRITE_REQUEST: + return "์—ฌํ–‰ ํ›„๊ธฐ์— ๋ฆฌ๋ทฐ๋ฅผ ๋‚จ๊ฒจ์ฃผ์„ธ์š”."; + default: + return "์•Œ๋ฆผ ํƒ€์ž…์ด ์ง€์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."; + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityContentReadController.java b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityContentReadController.java new file mode 100644 index 00000000..edf494b2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityContentReadController.java @@ -0,0 +1,135 @@ +package com.haejwo.tripcometrue.domain.city.controller; + +import com.haejwo.tripcometrue.domain.city.dto.response.*; +import com.haejwo.tripcometrue.domain.city.service.CityContentReadService; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import com.haejwo.tripcometrue.global.validator.annotation.HomeTopListQueryType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/v1/cities") +@RestController +public class CityContentReadController { + + private final CityContentReadService cityContentReadService; + + // ํ™ˆํ”ผ๋“œ TOP ์ธ๊ธฐ ๋„์‹œ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @GetMapping("/top-list") + public ResponseEntity>> getTopCityList( + @RequestParam @HomeTopListQueryType String type + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + cityContentReadService.getTopCityList(type) + ) + ); + } + + // ๋„์‹œ ๊ฐค๋Ÿฌ๋ฆฌ ๋”๋ณด๊ธฐ ์กฐํšŒ + @GetMapping("/{cityId}/images") + public ResponseEntity>> getImagesByCityIdPagination( + @PathVariable("cityId") Long cityId, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + cityContentReadService.getImages(cityId, pageable) + ) + ); + } + + // ๋„์‹œ ๊ฐค๋Ÿฌ๋ฆฌ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @GetMapping("/{cityId}/images/list") + public ResponseEntity>> getImagesByCityId( + @PathVariable("cityId") Long cityId + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + cityContentReadService.getImages(cityId) + ) + ); + } + + // ๋„์‹œ ์‡ผ์ธ  ๋”๋ณด๊ธฐ ์กฐํšŒ + @GetMapping("/{cityId}/videos") + public ResponseEntity>> getVideosByCityIdPagination( + @PathVariable("cityId") Long cityId, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + cityContentReadService.getVideos(cityId, pageable) + ) + ); + } + + // ๋„์‹œ ์‡ผ์ธ  ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @GetMapping("/{cityId}/videos/list") + public ResponseEntity>> getVideosByCityId( + @PathVariable("cityId") Long cityId + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + cityContentReadService.getVideos(cityId) + ) + ); + } + + // ๋„์‹œ ํ•ซํ”Œ๋ ˆ์ด์Šค ์กฐํšŒ + @GetMapping("/{cityId}/hot-places") + public ResponseEntity>> getHotPlaces( + @PathVariable("cityId") Long cityId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDTO.okWithData(cityContentReadService.getHotPlaces(cityId)) + ); + } + + // ๋„์‹œ ์—ฌํ–‰์ง€ ๋”๋ณด๊ธฐ ์กฐํšŒ ๋ฐ ๋„์‹œ ์—ฌํ–‰์ง€ ๊ฒ€์ƒ‰ ์กฐํšŒ (ํŽ˜์ด์ง•) + @GetMapping("/{cityId}/places") + public ResponseEntity>> getPlacesByCityIdPagination( + @PathVariable("cityId") Long cityId, + @RequestParam(required = false, name = "placeName") String placeName, + @PageableDefault(sort = "storedCount", direction = Sort.Direction.DESC) Pageable pageable + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDTO.okWithData(cityContentReadService.getPlaces(cityId, placeName, pageable)) + ); + } + + // ์œ„/๊ฒฝ๋„ ์ •๋ณด ํฌํ•จ ๋„์‹œ ์—ฌํ–‰์ง€ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @GetMapping("/{cityId}/places/list") + public ResponseEntity>> getPlacesWithLatLongByCityId( + @PathVariable("cityId") Long cityId + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDTO.okWithData(cityContentReadService.getPlacesWithLatLong(cityId)) + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityEditController.java b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityEditController.java new file mode 100644 index 00000000..ae9fd0e4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityEditController.java @@ -0,0 +1,42 @@ +package com.haejwo.tripcometrue.domain.city.controller; + +import com.haejwo.tripcometrue.domain.city.dto.request.AddCityRequestDto; +import com.haejwo.tripcometrue.domain.city.dto.response.CityResponseDto; +import com.haejwo.tripcometrue.domain.city.service.CityEditService; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RequestMapping("/v1/cities") +@RestController +public class CityEditController { + + private final CityEditService cityEditService; + + @PostMapping + public ResponseEntity> addCity( + @RequestBody AddCityRequestDto request + ) { + + return ResponseEntity + .status(HttpStatus.CREATED) + .body( + ResponseDTO.okWithData(cityEditService.addCity(request)) + ); + } + + @DeleteMapping("/{cityId}") + public ResponseEntity> deleteCity( + @PathVariable("cityId") Long cityId + ) { + + cityEditService.deleteCity(cityId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(ResponseDTO.ok()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityInfoReadController.java b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityInfoReadController.java new file mode 100644 index 00000000..b28c9c56 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CityInfoReadController.java @@ -0,0 +1,57 @@ +package com.haejwo.tripcometrue.domain.city.controller; + +import com.haejwo.tripcometrue.domain.city.dto.response.CityInfoResponseDto; +import com.haejwo.tripcometrue.domain.city.dto.response.ExchangeRateResponseDto; +import com.haejwo.tripcometrue.domain.city.dto.response.WeatherResponseDto; +import com.haejwo.tripcometrue.domain.city.service.CityInfoReadService; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/v1/cities") +@RestController +public class CityInfoReadController { + + private final CityInfoReadService cityInfoReadService; + + @GetMapping("/{cityId}") + public ResponseEntity> getCityInfo( + @PathVariable("cityId") Long cityId + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDTO.okWithData(cityInfoReadService.getCityInfo(cityId)) + ); + } + + @GetMapping("/{cityId}/exchange-rates") + public ResponseEntity> getExchangeRate( + @PathVariable("cityId") Long cityId + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDTO.okWithData(cityInfoReadService.getExchangeRateByCityId(cityId)) + ); + } + + @GetMapping("/{cityId}/weathers") + public ResponseEntity>> getWeatherInfo( + @PathVariable("cityId") Long cityId + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ResponseDTO.okWithData(cityInfoReadService.getWeatherInfo(cityId)) + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CitySearchController.java b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CitySearchController.java new file mode 100644 index 00000000..3a8ec146 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/controller/CitySearchController.java @@ -0,0 +1,34 @@ +package com.haejwo.tripcometrue.domain.city.controller; + +import com.haejwo.tripcometrue.domain.city.dto.response.CityResponseDto; +import com.haejwo.tripcometrue.domain.city.service.CitySearchService; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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 java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/v1/cities") +@RestController +public class CitySearchController { + + private final CitySearchService citySearchService; + + @GetMapping + public ResponseEntity>> search( + @RequestParam("name") String name + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + citySearchService.search(name) + ) + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/api/ExchangeRateApiDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/api/ExchangeRateApiDto.java new file mode 100644 index 00000000..6c8837ad --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/api/ExchangeRateApiDto.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.city.dto.api; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ExchangeRateApiDto( + @JsonProperty("result") + Integer result, + @JsonProperty("cur_unit") + String curUnit, + @JsonProperty("cur_nm") + String curName, + @JsonProperty("deal_bas_r") + String dealBaseRate +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/request/AddCityRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/request/AddCityRequestDto.java new file mode 100644 index 00000000..304c5fe8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/request/AddCityRequestDto.java @@ -0,0 +1,33 @@ +package com.haejwo.tripcometrue.domain.city.dto.request; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.global.enums.Country; + +public record AddCityRequestDto( + String name, + String englishName, + String language, + String timeDifference, + String voltage, + String visa, + String currency, + String weatherRecommendation, + String weatherDescription, + Country country +) { + public City toEntity() { + return City.builder() + .name(this.name) + .englishName(this.englishName) + .language(this.language) + .timeDifference(this.timeDifference) + .voltage(this.voltage) + .visa(this.visa) + .currency(CurrencyUnit.valueOf(this.currency)) + .weatherRecommendation(this.weatherRecommendation) + .weatherDescription(this.weatherDescription) + .country(this.country) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityImageContentResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityImageContentResponseDto.java new file mode 100644 index 00000000..ed2e205a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityImageContentResponseDto.java @@ -0,0 +1,36 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import lombok.Builder; + +public record CityImageContentResponseDto( + Long imageId, + Long tripRecordId, + String imageUrl, + Integer tripRecordStoreCount +) { + @Builder + public CityImageContentResponseDto { + } + + public static CityImageContentResponseDto fromEntity(TripRecordSchedule entity, TripRecordScheduleImage image) { + return CityImageContentResponseDto.builder() + .imageId(image.getId()) + .tripRecordId(entity.getTripRecord().getId()) + .imageUrl(image.getImageUrl()) + .tripRecordStoreCount(entity.getTripRecord().getStoreCount()) + .build(); + } + + public static CityImageContentResponseDto fromEntity(TripRecordScheduleImage entity) { + TripRecord tripRecord = entity.getTripRecordSchedule().getTripRecord(); + return CityImageContentResponseDto.builder() + .imageId(entity.getId()) + .tripRecordId(tripRecord.getId()) + .imageUrl(entity.getImageUrl()) + .tripRecordStoreCount(tripRecord.getStoreCount()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityInfoResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityInfoResponseDto.java new file mode 100644 index 00000000..facac4e5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityInfoResponseDto.java @@ -0,0 +1,39 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import lombok.Builder; + +import java.util.Objects; + +public record CityInfoResponseDto( + Long id, + String name, + String language, + String timeDifference, + String voltage, + String visa, + String curUnit, + String curName +) { + + @Builder + public CityInfoResponseDto { + } + + public static CityInfoResponseDto fromEntity(City entity) { + return CityInfoResponseDto.builder() + .id(entity.getId()) + .name(entity.getName()) + .language(entity.getLanguage()) + .timeDifference(entity.getTimeDifference()) + .voltage(entity.getVoltage()) + .visa(entity.getVisa()) + .curUnit( + Objects.nonNull(entity.getCurrency()) ? entity.getCurrency().name() : null + ) + .curName( + Objects.nonNull(entity.getCurrency()) ? entity.getCurrency().getCurrencyName() : null + ) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityPlaceResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityPlaceResponseDto.java new file mode 100644 index 00000000..3224b16f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityPlaceResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import lombok.Builder; + +public record CityPlaceResponseDto( + Long placeId, + String placeName, + Integer storedCount, + Integer commentTotal, + String imageUrl +) { + + @Builder + public CityPlaceResponseDto { + } + + public static CityPlaceResponseDto fromEntity(Place entity, String imageUrl) { + return CityPlaceResponseDto.builder() + .placeId(entity.getId()) + .placeName(entity.getName()) + .storedCount(entity.getStoredCount()) + .commentTotal(entity.getCommentCount()) + .imageUrl(imageUrl) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityPlaceWithLatLongResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityPlaceWithLatLongResponseDto.java new file mode 100644 index 00000000..59bfc3e6 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityPlaceWithLatLongResponseDto.java @@ -0,0 +1,31 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import lombok.Builder; + +public record CityPlaceWithLatLongResponseDto( + Long placeId, + String placeName, + Integer storedCount, + Integer commentTotal, + String imageUrl, + Double latitude, + Double longitude +) { + + @Builder + public CityPlaceWithLatLongResponseDto { + } + + public static CityPlaceWithLatLongResponseDto fromEntity(Place entity, String imageUrl) { + return CityPlaceWithLatLongResponseDto.builder() + .placeId(entity.getId()) + .placeName(entity.getName()) + .storedCount(entity.getStoredCount()) + .commentTotal(entity.getCommentCount()) + .imageUrl(imageUrl) + .latitude(entity.getLatitude()) + .longitude(entity.getLongitude()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityResponseDto.java new file mode 100644 index 00000000..39cf3928 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/CityResponseDto.java @@ -0,0 +1,67 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import lombok.Builder; + +import java.util.Objects; + +public record CityResponseDto( + Long cityId, + String name, + String englishName, + String language, + String timeDifference, + String voltage, + String visa, + String curUnit, + String exchangeRate, + String weatherRecommendation, + String weatherDescription, + String country, + String imageUrl +) { + + @Builder + public CityResponseDto { + } + + public static CityResponseDto fromEntity(City entity) { + return CityResponseDto.builder() + .cityId(entity.getId()) + .name(entity.getName()) + .englishName(entity.getEnglishName()) + .language(entity.getLanguage()) + .timeDifference(entity.getTimeDifference()) + .voltage(entity.getVoltage()) + .visa(entity.getVisa()) + .curUnit( + Objects.nonNull(entity.getCurrency()) ? entity.getCurrency().name() : null + ) + .exchangeRate(null) + .weatherRecommendation(entity.getWeatherRecommendation()) + .weatherDescription(entity.getWeatherDescription()) + .country(entity.getCountry().getDescription()) + .imageUrl(entity.getImageUrl()) + .build(); + } + + public static CityResponseDto fromEntity(City entity, String exchangeRate) { + return CityResponseDto.builder() + .cityId(entity.getId()) + .name(entity.getName()) + .englishName(entity.getEnglishName()) + .language(entity.getLanguage()) + .timeDifference(entity.getTimeDifference()) + .voltage(entity.getVoltage()) + .visa(entity.getVisa()) + .curUnit( + Objects.nonNull(entity.getCurrency()) ? entity.getCurrency().name() : null + ) + .exchangeRate(exchangeRate) + .weatherRecommendation(entity.getWeatherRecommendation()) + .weatherDescription(entity.getWeatherDescription()) + .country(entity.getCountry().getDescription()) + .imageUrl(entity.getImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/ExchangeRateResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/ExchangeRateResponseDto.java new file mode 100644 index 00000000..5f620bd3 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/ExchangeRateResponseDto.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import lombok.Builder; + +public record ExchangeRateResponseDto( + String curUnit, + String curName, + String exchangeRate, + String country +) { + + @Builder + public ExchangeRateResponseDto { + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/TopCityResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/TopCityResponseDto.java new file mode 100644 index 00000000..7419a070 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/TopCityResponseDto.java @@ -0,0 +1,25 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import lombok.Builder; + +public record TopCityResponseDto( + Long cityId, + String cityName, + Integer storedCount, + String imageUrl +) { + + @Builder + public TopCityResponseDto { + } + + public static TopCityResponseDto fromEntity(City entity) { + return TopCityResponseDto.builder() + .cityId(entity.getId()) + .cityName(entity.getName()) + .storedCount(entity.getStoreCount()) + .imageUrl(entity.getImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/WeatherResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/WeatherResponseDto.java new file mode 100644 index 00000000..5bb8ef6a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/dto/response/WeatherResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.city.dto.response; + +import com.haejwo.tripcometrue.domain.city.entity.Weather; +import lombok.Builder; + +public record WeatherResponseDto( + Integer month, + String maxAvgTempC, + String minAvgTempC, + String maxAvgTempF, + String minAvgTempF +) { + + @Builder + public WeatherResponseDto { + } + + public static WeatherResponseDto fromEntity(Weather entity, String maxAvgTempF, String minAvgTempF) { + return WeatherResponseDto.builder() + .month(entity.getMonth()) + .maxAvgTempC(entity.getMaxAvgTemp()) + .minAvgTempC(entity.getMinAvgTemp()) + .maxAvgTempF(maxAvgTempF) + .minAvgTempF(minAvgTempF) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/entity/City.java b/src/main/java/com/haejwo/tripcometrue/domain/city/entity/City.java new file mode 100644 index 00000000..bcf5ff4b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/entity/City.java @@ -0,0 +1,80 @@ +package com.haejwo.tripcometrue.domain.city.entity; + +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import com.haejwo.tripcometrue.global.enums.Country; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class City extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "city_id") + private Long id; + private String name; + private String englishName; + private String language; + private String timeDifference; + private String voltage; + private String visa; + @Column(length = 1000) + private String weatherRecommendation; + @Column(length = 2500) + private String weatherDescription; + private Integer storeCount; + private String imageUrl; + + @Enumerated(EnumType.STRING) + private CurrencyUnit currency; + + @Enumerated(EnumType.STRING) + private Country country; + + @Builder + private City( + Long id, String name, String englishName, String language, String timeDifference, + String voltage, String visa, CurrencyUnit currency, String weatherRecommendation, + String weatherDescription, Integer storeCount, Country country, String imageUrl + ) { + this.id = id; + this.name = name; + this.englishName = englishName; + this.language = language; + this.timeDifference = timeDifference; + this.voltage = voltage; + this.visa = visa; + this.currency = currency; + this.weatherRecommendation = weatherRecommendation; + this.weatherDescription = weatherDescription; + this.storeCount = storeCount; + this.country = country; + this.imageUrl = imageUrl; + } + + @PrePersist + public void prePersist() { + this.storeCount = 0; + } + + public void incrementStoreCount() { + if(this.storeCount == null) { + this.storeCount = 1; + } else { + this.storeCount++; + } + } + + public void decrementStoreCount() { + if(this.storeCount > 0) { + this.storeCount--; + } + } +} + diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/entity/Weather.java b/src/main/java/com/haejwo/tripcometrue/domain/city/entity/Weather.java new file mode 100644 index 00000000..8df5e9b7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/entity/Weather.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.domain.city.entity; + +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Weather extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer month; + + private String maxAvgTemp; + + private String minAvgTemp; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "city_id") + private City city; + + @Builder + private Weather(Long id, City city, Integer month, + String maxAvgTemp, String minAvgTemp) { + this.id = id; + this.city = city; + this.month = month; + this.maxAvgTemp = maxAvgTemp; + this.minAvgTemp = minAvgTemp; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/exception/CityNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/city/exception/CityNotFoundException.java new file mode 100644 index 00000000..f6c39d76 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/exception/CityNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.city.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class CityNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.CITY_NOT_FOUND; + + public CityNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/exception/ExchangeRateApiCallFailException.java b/src/main/java/com/haejwo/tripcometrue/domain/city/exception/ExchangeRateApiCallFailException.java new file mode 100644 index 00000000..746d8d7a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/exception/ExchangeRateApiCallFailException.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.city.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class ExchangeRateApiCallFailException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.EXCHANGE_RATE_API_FAIL; + public ExchangeRateApiCallFailException() { + super(ERROR_CODE); + } + + public ExchangeRateApiCallFailException(String message) { + super(ERROR_CODE, message); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepository.java new file mode 100644 index 00000000..61f8c54c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepository.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.city.repository; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.Country; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CityRepository + extends JpaRepository, CityRepositoryCustom { + + Optional findByNameAndCountry(String name, Country country); + List findAllByCountry(Country country); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepositoryCustom.java new file mode 100644 index 00000000..cb307312 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepositoryCustom.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.city.repository; + +import com.haejwo.tripcometrue.domain.city.entity.City; + +import java.util.List; + +public interface CityRepositoryCustom { + + List findTopCityListDomestic(int size); + + List findTopCityListOverseas(int size); + + List findBySearchParams(String name); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepositoryImpl.java new file mode 100644 index 00000000..7aff5283 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/CityRepositoryImpl.java @@ -0,0 +1,63 @@ +package com.haejwo.tripcometrue.domain.city.repository; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.Country; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.util.StringUtils; + +import java.util.List; + +import static com.haejwo.tripcometrue.domain.city.entity.QCity.city; + +@RequiredArgsConstructor +public class CityRepositoryImpl implements CityRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findTopCityListDomestic(int size) { + return queryFactory + .selectFrom(city) + .where(city.country.eq(Country.KOREA)) + .orderBy(city.storeCount.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findTopCityListOverseas(int size) { + return queryFactory + .selectFrom(city) + .where(city.country.eq(Country.KOREA).not()) + .orderBy(city.storeCount.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findBySearchParams(String name) { + return queryFactory + .selectFrom(city) + .where( + containsIgnoreCaseName(name) + ) + .orderBy(city.storeCount.desc()) + .fetch(); + } + + private BooleanExpression containsIgnoreCaseName(String name) { + + if (!StringUtils.hasText(name)) { + return null; + } + + String replacedName = name.replaceAll(" ", ""); + + return Expressions.stringTemplate( + "function('replace',{0},{1},{2})", city.name, " ", "" + ).containsIgnoreCase(replacedName); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/repository/WeatherRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/WeatherRepository.java new file mode 100644 index 00000000..1f8d225f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/repository/WeatherRepository.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.city.repository; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.entity.Weather; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface WeatherRepository extends JpaRepository { + + List findAllByCityAndMonthInOrderByMonthAsc(City city, List months); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityContentReadService.java b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityContentReadService.java new file mode 100644 index 00000000..8b698139 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityContentReadService.java @@ -0,0 +1,178 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.domain.city.dto.response.*; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleImageWithPlaceIdQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image.TripRecordScheduleImageRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video.TripRecordScheduleVideoRepository; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +public class CityContentReadService { + + private final CityRepository cityRepository; + private final PlaceRepository placeRepository; + private final TripRecordScheduleImageRepository tripRecordScheduleImageRepository; + private final TripRecordScheduleVideoRepository tripRecordScheduleVideoRepository; + + private static final int CITY_HOT_PLACES_SIZE = 10; + private static final int CITY_MEDIA_CONTENT_SIZE = 10; + private static final int HOME_CONTENT_SIZE = 5; + + // ํ™ˆํ”ผ๋“œ ์ธ๊ธฐ ๋„์‹œ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + public List getTopCityList(String type) { + if (type.equalsIgnoreCase("domestic")) { + return cityRepository + .findTopCityListDomestic(HOME_CONTENT_SIZE) + .stream() + .map(TopCityResponseDto::fromEntity) + .toList(); + } else { + return cityRepository + .findTopCityListOverseas(HOME_CONTENT_SIZE) + .stream() + .map(TopCityResponseDto::fromEntity) + .toList(); + } + } + + // ๋„์‹œ ํ•ซํ”Œ๋ ˆ์ด์Šค ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @Transactional(readOnly = true) + public List getHotPlaces(Long id) { + City city = getCityEntity(id); + + List places = placeRepository + .findPlacesByCityAndOrderByStoredCountLimitSize(city, CITY_HOT_PLACES_SIZE); + + Map> imageMap = getImageMapFromPlaceIds(places); + + return places + .stream() + .map(place -> { + String imageUrl = Objects.isNull(imageMap.get(place.getId())) ? null : + imageMap.get(place.getId()) + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(TripRecordScheduleImageWithPlaceIdQueryDto::imageUrl) + .orElse(null); + + return CityPlaceResponseDto.fromEntity(place, imageUrl); + }) + .toList(); + } + + // ์œ„/๊ฒฝ๋„ ์ •๋ณด ํฌํ•จ ๋„์‹œ ์—ฌํ–‰์ง€ ์ „์ฒด ์กฐํšŒ + @Transactional(readOnly = true) + public List getPlacesWithLatLong(Long cityId) { + List places = placeRepository.findByCityId(cityId); + + Map> imageMap = getImageMapFromPlaceIds(places); + + return places + .stream() + .map(place -> { + String imageUrl = Objects.isNull(imageMap.get(place.getId())) ? null : + imageMap.get(place.getId()) + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(TripRecordScheduleImageWithPlaceIdQueryDto::imageUrl) + .orElse(null); + + return CityPlaceWithLatLongResponseDto.fromEntity(place, imageUrl); + }) + .toList(); + } + + // ๋„์‹œ ์—ฌํ–‰์ง€ ์ „์ฒด ์กฐํšŒ ๋ฐ ๊ฒ€์ƒ‰ (์Šคํฌ๋กค ํŽ˜์ด์ง•) + @Transactional(readOnly = true) + public SliceResponseDto getPlaces(Long cityId, String placeName, Pageable pageable) { + Slice places = placeRepository.findPlacesByCityIdAndPlaceName(cityId, placeName, pageable); + + Map> imageMap = getImageMapFromPlaceIds(places.getContent()); + + return SliceResponseDto.of( + places + .map(place -> { + String imageUrl = Objects.isNull(imageMap.get(place.getId())) ? null : + imageMap.get(place.getId()) + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(TripRecordScheduleImageWithPlaceIdQueryDto::imageUrl) + .orElse(null); + + return CityPlaceResponseDto.fromEntity(place, imageUrl); + }) + ); + } + + // ๋„์‹œ ๊ฐค๋Ÿฌ๋ฆฌ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @Transactional(readOnly = true) + public List getImages(Long cityId) { + return tripRecordScheduleImageRepository.findByCityIdOrderByCreatedAtDescLimitSize(cityId, CITY_MEDIA_CONTENT_SIZE) + .stream() + .map(CityImageContentResponseDto::fromEntity) + .toList(); + } + + // ๋„์‹œ ๊ฐค๋Ÿฌ๋ฆฌ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ (ํŽ˜์ด์ง•, ์ •๋ ฌ) + @Transactional(readOnly = true) + public SliceResponseDto getImages(Long cityId, Pageable pageable) { + return SliceResponseDto.of( + tripRecordScheduleImageRepository.findByCityId(cityId, pageable) + .map(CityImageContentResponseDto::fromEntity) + ); + } + + // ๋„์‹œ ์‡ผ์ธ  ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @Transactional(readOnly = true) + public List getVideos(Long cityId) { + return tripRecordScheduleVideoRepository + .findByCityIdOrderByCreatedAtDescLimitSize(cityId, CITY_MEDIA_CONTENT_SIZE) + .stream() + .map(TripRecordScheduleVideoListItemResponseDto::fromQueryDto) + .toList(); + } + + // ๋„์‹œ ์‡ผ์ธ  ๋ฆฌ์ŠคํŠธ ์กฐํšŒ (ํŽ˜์ด์ง•, ์ •๋ ฌ) + @Transactional(readOnly = true) + public SliceResponseDto getVideos(Long cityId, Pageable pageable) { + return SliceResponseDto.of( + tripRecordScheduleVideoRepository + .findByCityId(cityId, pageable) + .map(TripRecordScheduleVideoListItemResponseDto::fromQueryDto) + ); + } + + private City getCityEntity(Long id) { + return cityRepository.findById(id) + .orElseThrow(CityNotFoundException::new); + } + + private Map> getImageMapFromPlaceIds(List places) { + List placeIds = places.stream().map(Place::getId).toList(); + + return tripRecordScheduleImageRepository + .findInPlaceIdsOrderByCreatedAtDesc(placeIds) + .stream() + .collect(Collectors.groupingBy(TripRecordScheduleImageWithPlaceIdQueryDto::placeId)); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityEditService.java b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityEditService.java new file mode 100644 index 00000000..b85f3dd9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityEditService.java @@ -0,0 +1,36 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.domain.city.dto.request.AddCityRequestDto; +import com.haejwo.tripcometrue.domain.city.dto.response.CityResponseDto; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CityEditService { + + private final CityRepository cityRepository; + + @Transactional + public CityResponseDto addCity(AddCityRequestDto request) { + return CityResponseDto.fromEntity( + cityRepository.save(request.toEntity()) + ); + } + + @Transactional + public void deleteCity(Long id) { + City city = getCityEntity(id); + + cityRepository.delete(city); + } + + private City getCityEntity(Long id) { + return cityRepository.findById(id) + .orElseThrow(CityNotFoundException::new); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityInfoReadService.java b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityInfoReadService.java new file mode 100644 index 00000000..9d3c6556 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CityInfoReadService.java @@ -0,0 +1,111 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.domain.city.dto.response.CityInfoResponseDto; +import com.haejwo.tripcometrue.domain.city.dto.response.ExchangeRateResponseDto; +import com.haejwo.tripcometrue.domain.city.dto.response.WeatherResponseDto; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.city.entity.Weather; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.city.repository.WeatherRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class CityInfoReadService { + + private final CityRepository cityRepository; + private final WeatherRepository weatherRepository; + private final RedisTemplate redisTemplate; + + private static final int WEATHER_MONTH_GAP = 3; + + @Transactional(readOnly = true) + public CityInfoResponseDto getCityInfo(Long id) { + return CityInfoResponseDto.fromEntity( + getCityEntity(id) + ); + } + + @Transactional(readOnly = true) + public ExchangeRateResponseDto getExchangeRateByCityId(Long cityId) { + + City city = cityRepository.findById(cityId) + .orElseThrow(CityNotFoundException::new); + + CurrencyUnit currencyUnit = city.getCurrency(); + String exchangeRate = (String) redisTemplate.opsForValue() + .get("exchange-rate:" + currencyUnit); + + return ExchangeRateResponseDto.builder() + .curUnit(currencyUnit.name()) + .curName(currencyUnit.getCurrencyName()) + .exchangeRate(exchangeRate) + .country(city.getCountry().getDescription()) + .build(); + } + + @Transactional(readOnly = true) + public List getWeatherInfo(Long id) { + City city = getCityEntity(id); + + // ํ˜„์žฌ ๋‹ฌ ํฌํ•จ ํ–ฅํ›„ 3๊ฐœ์›” + int curMonth = LocalDate.now().getMonthValue(); + List months = new ArrayList<>(); + for (int i = 0; i <= WEATHER_MONTH_GAP; i++) { + months.add(((curMonth + i) % 13) + 1); + } + + List weathers = weatherRepository.findAllByCityAndMonthInOrderByMonthAsc(city, months); + + return sortWeatherInfos(weathers) + .stream() + .map( + w -> WeatherResponseDto.fromEntity(w, convertToTempF(w.getMaxAvgTemp()), convertToTempF(w.getMinAvgTemp())) + ).toList(); + } + + private City getCityEntity(Long id) { + return cityRepository.findById(id) + .orElseThrow(CityNotFoundException::new); + } + + private List sortWeatherInfos(List weathers) { + int lastIdx = weathers.size() - 1; + + if(weathers.get(lastIdx).getMonth() - weathers.get(0).getMonth() > WEATHER_MONTH_GAP) { + weathers.add(0, weathers.remove(lastIdx)); + + for (int i = lastIdx; i >= 0; i--) { + int last = weathers.get(lastIdx).getMonth(); + int prev = weathers.get(lastIdx - 1).getMonth(); + + weathers.add(0, weathers.remove(lastIdx)); + + if (last - prev != 1) { + break; + } + } + } + + return weathers; + } + + private String convertToTempF(String tempC) { + return new BigDecimal(tempC) + .multiply(new BigDecimal("1.8")) + .add(new BigDecimal("32")) + .setScale(2, RoundingMode.HALF_UP) + .toString(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/service/CitySearchService.java b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CitySearchService.java new file mode 100644 index 00000000..abea4ce1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/service/CitySearchService.java @@ -0,0 +1,45 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.domain.city.dto.response.CityResponseDto; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +@Service +public class CitySearchService { + + private final CityRepository cityRepository; + private final RedisTemplate redisTemplate; + private static final String EXCHANGE_RATE_REDIS_KEY_PREFIX = "exchange-rate:"; + + @Transactional(readOnly = true) + public List search(String name) { + return cityRepository + .findBySearchParams(name) + .stream() + .map(city -> { + String exchangeRate = getExchangeRate(city); + return CityResponseDto.fromEntity(city, exchangeRate); + } + ) + .toList(); + } + + private String getExchangeRate(City city) { + CurrencyUnit currency = city.getCurrency(); + if (Objects.isNull(currency)) { + return null; + } + + return (String) redisTemplate.opsForValue() + .get(EXCHANGE_RATE_REDIS_KEY_PREFIX + currency.name()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/service/ExchangeRateApiCaller.java b/src/main/java/com/haejwo/tripcometrue/domain/city/service/ExchangeRateApiCaller.java new file mode 100644 index 00000000..1fcac8fb --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/service/ExchangeRateApiCaller.java @@ -0,0 +1,71 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.domain.city.dto.api.ExchangeRateApiDto; +import com.haejwo.tripcometrue.domain.city.exception.ExchangeRateApiCallFailException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; + +@RequiredArgsConstructor +@Service +public class ExchangeRateApiCaller { + + private final RestTemplate restTemplate; + + @Value("${exchange-rate.api.url}") + private String API_URL; + + @Value("${exchange-rate.api.key}") + private String API_KEY; + + public List call() { + + URI uri = buildUri(LocalDate.now()); + + try { + ResponseEntity> responseEntity = restTemplate.exchange( + uri, HttpMethod.GET, null, new ParameterizedTypeReference<>() {} + ); + + if(Objects.nonNull(responseEntity.getBody()) && responseEntity.getBody().get(0).result() != 1) { + Integer result = responseEntity.getBody().get(0).result(); + String errorMessage = switch (result) { + case 2 -> "DATA ์ฝ”๋“œ ์˜ค๋ฅ˜"; + case 3 -> "์ธ์ฆ์ฝ”๋“œ ์˜ค๋ฅ˜"; + case 4 -> "์ผ์ผ์ œํ•œํšŸ์ˆ˜ ๋งˆ๊ฐ"; + default -> "ํ™˜์œจ API ํ˜ธ์ถœ ์‹คํŒจ"; + }; + + throw new ExchangeRateApiCallFailException(errorMessage); + } + + return responseEntity.getBody(); + } catch (Exception e) { + throw new ExchangeRateApiCallFailException(); + } + } + + private URI buildUri(LocalDate date) { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + + return UriComponentsBuilder + .fromHttpUrl(API_URL) + .queryParam("authkey", API_KEY) + .queryParam("searchdate", dateFormatter.format(date)) + .queryParam("data", "AP01") + .build() + .encode() + .toUri(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/city/service/ExchangeRateUpdateScheduler.java b/src/main/java/com/haejwo/tripcometrue/domain/city/service/ExchangeRateUpdateScheduler.java new file mode 100644 index 00000000..0e4aacb6 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/city/service/ExchangeRateUpdateScheduler.java @@ -0,0 +1,52 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.domain.city.dto.api.ExchangeRateApiDto; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class ExchangeRateUpdateScheduler { + + private final ExchangeRateApiCaller exchangeRateApiCaller; + private final RedisTemplate redisTemplate; + private static final String EXCHANGE_RATE_REDIS_KEY_PREFIX = "exchange-rate:"; + + // ๋งค์ผ 9:00, 12:00, 15:00 ํ™˜์œจ ์ •๋ณด ์—…๋ฐ์ดํŠธ + @Scheduled(cron = "0 0 9,12,15 * * *") + public void update() { + List exchangeRates = exchangeRateApiCaller.call(); + + for (ExchangeRateApiDto exchangeRate : exchangeRates) { + String curUnitString = exchangeRate.curUnit().trim().substring(0, 3); + + if(curUnitString.equals("KRW")) continue; + + saveExchangeRateToRedis(exchangeRate, curUnitString); + } + } + + private void saveExchangeRateToRedis(ExchangeRateApiDto exchangeRate, String curUnitString) { + ValueOperations operations = redisTemplate.opsForValue(); + CurrencyUnit currencyUnit = CurrencyUnit.valueOf(curUnitString); + + String key = EXCHANGE_RATE_REDIS_KEY_PREFIX + currencyUnit; + String value = currencyUnit.getStandard() + ":" + exchangeRate.dealBaseRate(); + + if (currencyUnit.getStandard() != 1) { + BigDecimal rate = new BigDecimal(exchangeRate.dealBaseRate()) + .divide(new BigDecimal(currencyUnit.getStandard())); + + value = "1:" + rate; + } + + operations.set(key, value); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/controller/PlaceReviewCommentController.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/controller/PlaceReviewCommentController.java new file mode 100644 index 00000000..794cb2b4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/controller/PlaceReviewCommentController.java @@ -0,0 +1,51 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.controller; + +import com.haejwo.tripcometrue.domain.comment.placereview.dto.request.PlaceReviewCommentRequestDto; +import com.haejwo.tripcometrue.domain.comment.placereview.service.PlaceReviewCommentService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/places/reviews") +public class PlaceReviewCommentController { + + private final PlaceReviewCommentService commentService; + + @PostMapping("/{placeReviewId}/comments") + public ResponseEntity> registerComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewId, + @RequestBody @Valid com.haejwo.tripcometrue.domain.comment.placereview.dto.request.PlaceReviewCommentRequestDto requestDto + ) { + + commentService.saveComment(principalDetails, placeReviewId, requestDto); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @PostMapping("/comments/{placeReviewCommentId}/reply-comments") + public ResponseEntity> registerReplyComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewCommentId, + @RequestBody @Valid PlaceReviewCommentRequestDto requestDto + ) { + + commentService.saveReplyComment(principalDetails, placeReviewCommentId, requestDto); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @DeleteMapping("/comments/{placeReviewCommentId}") + public ResponseEntity> deleteComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewCommentId + ) { + + commentService.removeComment(principalDetails, placeReviewCommentId); + return ResponseEntity.ok(ResponseDTO.ok()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/controller/PlaceReviewCommentControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/controller/PlaceReviewCommentControllerAdvice.java new file mode 100644 index 00000000..35553e16 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/controller/PlaceReviewCommentControllerAdvice.java @@ -0,0 +1,21 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.controller; + +import com.haejwo.tripcometrue.domain.comment.placereview.exception.PlaceReviewCommentNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class PlaceReviewCommentControllerAdvice { + + @ExceptionHandler(PlaceReviewCommentNotFoundException.class) + public ResponseEntity> handlePlaceReviewCommentNotFoundException(PlaceReviewCommentNotFoundException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/request/PlaceReviewCommentRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/request/PlaceReviewCommentRequestDto.java new file mode 100644 index 00000000..ee7f37b1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/request/PlaceReviewCommentRequestDto.java @@ -0,0 +1,39 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.dto.request; + +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; + +public record PlaceReviewCommentRequestDto( + + @NotNull(message = "๋ณธ๋ฌธ์€ ํ•„์ˆ˜๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Length(min = 1, max = 500, message = "์ž‘์„ฑ ํ—ˆ์šฉ ๋ฒ”์œ„๋Š” ์ตœ์†Œ 1์ž ๋˜๋Š” ์ตœ๋Œ€ 500์ž ์ž…๋‹ˆ๋‹ค.") + String content + +) { + + public static PlaceReviewComment toComment(Member member, PlaceReview placeReview, PlaceReviewCommentRequestDto requestDto) { + return PlaceReviewComment.builder() + .member(member) + .placeReview(placeReview) + .content(requestDto.content) + .build(); + } + + public static PlaceReviewComment toReplyComment( + Member member, + PlaceReview placeReview, + PlaceReviewComment placeReviewComment, + PlaceReviewCommentRequestDto requestDto + ) { + + return PlaceReviewComment.builder() + .member(member) + .placeReview(placeReview) + .parentComment(placeReviewComment) + .content(requestDto.content) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/response/PlaceReviewCommentListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/response/PlaceReviewCommentListResponseDto.java new file mode 100644 index 00000000..6bb7b81d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/response/PlaceReviewCommentListResponseDto.java @@ -0,0 +1,30 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.dto.response; + +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import org.springframework.data.domain.Slice; + +import java.util.List; +import java.util.Objects; + +public record PlaceReviewCommentListResponseDto( + + int totalCount, + List comments + +) { + + public static PlaceReviewCommentListResponseDto fromData(int totalCount, Slice placeReviewComments, Member loginMember) { + return new PlaceReviewCommentListResponseDto( + totalCount, + placeReviewComments.map(placeReviewComment -> { + if (placeReviewComment.getParentComment() == null) { //์ตœ์ƒ์œ„ ๋Œ“๊ธ€๋งŒ ํฌํ•จ + return PlaceReviewCommentResponseDto.fromEntity(placeReviewComment, loginMember); + } + return null; + }) + .filter(Objects::nonNull) + .toList() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/response/PlaceReviewCommentResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/response/PlaceReviewCommentResponseDto.java new file mode 100644 index 00000000..bc8e21f9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/dto/response/PlaceReviewCommentResponseDto.java @@ -0,0 +1,54 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +public record PlaceReviewCommentResponseDto( + + Long commentId, + Long memberId, + String profileUrl, + String nickname, + boolean isWriter, + String content, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt, + + List replyComments + +) { + + public static PlaceReviewCommentResponseDto fromEntity(PlaceReviewComment placeReviewComment, Member loginMember) { + return new PlaceReviewCommentResponseDto( + placeReviewComment.getId(), + placeReviewComment.getMember().getId(), + placeReviewComment.getMember().getProfileImage(), + placeReviewComment.getMember().getMemberBase().getNickname(), + isWriter(placeReviewComment, loginMember), + placeReviewComment.getContent(), + placeReviewComment.getCreatedAt(), + getReplyComments(placeReviewComment, loginMember) //์ž์‹ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ์— ๋‹ด๊ธฐ + ); + } + + private static boolean isWriter(PlaceReviewComment placeReviewComment, Member loginMember) { + return Objects.equals(placeReviewComment.getMember(), loginMember); + } + + private static List getReplyComments(PlaceReviewComment placeReviewComment, Member loginMember) { + if (placeReviewComment.getParentComment() == null) { + return placeReviewComment.getChildComments().stream() + .map(comment -> PlaceReviewCommentResponseDto.fromEntity(comment, loginMember)) + .sorted(Comparator.comparing(PlaceReviewCommentResponseDto::createdAt).reversed()) + .toList(); + } + return null; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/entity/PlaceReviewComment.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/entity/PlaceReviewComment.java new file mode 100644 index 00000000..eb0c08d0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/entity/PlaceReviewComment.java @@ -0,0 +1,53 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.REMOVE; +import static jakarta.persistence.FetchType.LAZY; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PlaceReviewComment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "place_review_comment_id") + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "place_review_id") + private PlaceReview placeReview; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "parent_comment_id") + private PlaceReviewComment parentComment; + + @OneToMany(mappedBy = "parentComment", cascade = REMOVE, orphanRemoval = true) + private List childComments = new ArrayList<>(); + + @Column(nullable = false) + private String content; + + @Builder + public PlaceReviewComment(Member member, PlaceReview placeReview, PlaceReviewComment parentComment, String content) { + this.member = member; + this.placeReview = placeReview; + this.parentComment = parentComment; + this.content = content; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/exception/PlaceReviewCommentNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/exception/PlaceReviewCommentNotFoundException.java new file mode 100644 index 00000000..8fdb9fa2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/exception/PlaceReviewCommentNotFoundException.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class PlaceReviewCommentNotFoundException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.PLACE_REVIEW_COMMENT_NOT_FOUND; + + public PlaceReviewCommentNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/repository/PlaceReviewCommentRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/repository/PlaceReviewCommentRepository.java new file mode 100644 index 00000000..816dcf03 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/repository/PlaceReviewCommentRepository.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.repository; + +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface PlaceReviewCommentRepository extends JpaRepository { + + Slice findByPlaceReviewOrderByCreatedAtDesc(PlaceReview placeReview, Pageable pageable); + + @Modifying + @Query("delete from PlaceReviewComment prc where prc.parentComment.id = :placeReviewCommentId") + int deleteChildrenByPlaceReviewCommentId(@Param("placeReviewCommentId") Long placeReviewCommentId); + + @Modifying + @Query("delete from PlaceReviewComment prc where prc.id = :placeReviewCommentId") + int deleteParentByPlaceReviewCommentId(@Param("placeReviewCommentId") Long placeReviewCommentId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/service/PlaceReviewCommentService.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/service/PlaceReviewCommentService.java new file mode 100644 index 00000000..e4e89fc7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/placereview/service/PlaceReviewCommentService.java @@ -0,0 +1,114 @@ +package com.haejwo.tripcometrue.domain.comment.placereview.service; + +import com.haejwo.tripcometrue.domain.alarm.entity.AlarmType; +import com.haejwo.tripcometrue.domain.alarm.service.AlarmService; +import com.haejwo.tripcometrue.domain.comment.placereview.dto.request.PlaceReviewCommentRequestDto; +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.comment.placereview.exception.PlaceReviewCommentNotFoundException; +import com.haejwo.tripcometrue.domain.comment.placereview.repository.PlaceReviewCommentRepository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.exception.UserInvalidAccessException; +import com.haejwo.tripcometrue.domain.member.exception.UserNotFoundException; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewNotFoundException; +import com.haejwo.tripcometrue.domain.review.placereview.repository.PlaceReviewRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class PlaceReviewCommentService { + + private final MemberRepository memberRepository; + private final PlaceReviewRepository placeReviewRepository; + private final PlaceReviewCommentRepository placeReviewCommentRepository; + private final AlarmService alarmService; + + public void saveComment( + PrincipalDetails principalDetails, + Long placeReviewId, + com.haejwo.tripcometrue.domain.comment.placereview.dto.request.PlaceReviewCommentRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + PlaceReview placeReview = getPlaceReviewById(placeReviewId); + + PlaceReviewComment comment = com.haejwo.tripcometrue.domain.comment.placereview.dto.request.PlaceReviewCommentRequestDto.toComment(loginMember, placeReview, requestDto); + placeReviewCommentRepository.save(comment); + placeReview.increaseCommentCount(); + + alarmService.addAlarm( + loginMember, + placeReview.getMember(), + AlarmType.NEW_PLACE_REVIEW_COMMENT, + placeReviewId, + comment.getId()); + } + + private Member getMember(PrincipalDetails principalDetails) { + return memberRepository.findById(principalDetails.getMember().getId()) + .orElseThrow(UserNotFoundException::new); + } + + private PlaceReview getPlaceReviewById(Long placeReviewId) { + return placeReviewRepository.findById(placeReviewId) + .orElseThrow(PlaceReviewNotFoundException::new); + } + + public void saveReplyComment( + PrincipalDetails principalDetails, + Long placeReviewCommentId, + PlaceReviewCommentRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + PlaceReviewComment placeReviewComment = getPlaceReviewCommentById(placeReviewCommentId); + PlaceReview placeReview = placeReviewComment.getPlaceReview(); + + PlaceReviewComment comment = PlaceReviewCommentRequestDto.toReplyComment(loginMember, placeReview, placeReviewComment, requestDto); + placeReviewCommentRepository.save(comment); + placeReview.increaseCommentCount(); + } + + private PlaceReviewComment getPlaceReviewCommentById(Long placeReviewCommentId) { + return placeReviewCommentRepository.findById(placeReviewCommentId) + .orElseThrow(PlaceReviewCommentNotFoundException::new); + } + + public void removeComment(PrincipalDetails principalDetails, Long placeReviewCommentId) { + + Member loginMember = getMember(principalDetails); + PlaceReviewComment placeReviewComment = getPlaceReviewComment(placeReviewCommentId); + PlaceReview placeReview = placeReviewComment.getPlaceReview(); + + validateRightMemberAccess(loginMember, placeReviewComment); + + int removedCount = getRemovedCount(placeReviewCommentId, placeReviewComment); + placeReview.decreaseCommentCount(removedCount); + } + + private PlaceReviewComment getPlaceReviewComment(Long placeReviewCommentId) { + return placeReviewCommentRepository.findById(placeReviewCommentId) + .orElseThrow(PlaceReviewCommentNotFoundException::new); + } + + private int getRemovedCount(Long placeReviewCommentId, PlaceReviewComment placeReviewComment) { + int childrenCount = placeReviewCommentRepository.deleteChildrenByPlaceReviewCommentId(placeReviewComment.getId()); + int parentCount = placeReviewCommentRepository.deleteParentByPlaceReviewCommentId(placeReviewCommentId); + return childrenCount + parentCount; + } + + private void validateRightMemberAccess(Member member, PlaceReviewComment placeReviewComment) { + if (!Objects.equals(placeReviewComment.getMember().getId(), member.getId())) { + throw new UserInvalidAccessException(); + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/controller/TripRecordCommentController.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/controller/TripRecordCommentController.java new file mode 100644 index 00000000..2f25d36e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/controller/TripRecordCommentController.java @@ -0,0 +1,68 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.controller; + +import com.haejwo.tripcometrue.domain.comment.triprecord.dto.request.TripRecordCommentRequestDto; +import com.haejwo.tripcometrue.domain.comment.triprecord.dto.response.TripRecordCommentListResponseDto; +import com.haejwo.tripcometrue.domain.comment.triprecord.service.TripRecordCommentService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.data.domain.Sort.Direction; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/trip-records") +public class TripRecordCommentController { + + private final TripRecordCommentService commentService; + + @PostMapping("/{tripRecordId}/comments") + public ResponseEntity> registerComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId, + @RequestBody @Valid TripRecordCommentRequestDto requestDto + ) { + + commentService.saveComment(principalDetails, tripRecordId, requestDto); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @PostMapping("/comments/{tripRecordCommentId}/reply-comments") + public ResponseEntity> registerReplyComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordCommentId, + @RequestBody @Valid TripRecordCommentRequestDto requestDto + ) { + + commentService.saveReplyComment(principalDetails, tripRecordCommentId, requestDto); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @GetMapping("/{tripRecordId}/comments") + public ResponseEntity> getCommentList( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId, + @PageableDefault(sort = "createdAt", direction = Direction.DESC) Pageable pageable + ) { + + TripRecordCommentListResponseDto responseDto = + commentService.getCommentList(principalDetails, tripRecordId, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @DeleteMapping("/comments/{tripRecordCommentId}") + public ResponseEntity> deleteComment( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordCommentId + ) { + + commentService.removeComment(principalDetails, tripRecordCommentId); + return ResponseEntity.ok(ResponseDTO.ok()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/controller/TripRecordCommentControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/controller/TripRecordCommentControllerAdvice.java new file mode 100644 index 00000000..0cb104ab --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/controller/TripRecordCommentControllerAdvice.java @@ -0,0 +1,21 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.controller; + +import com.haejwo.tripcometrue.domain.comment.triprecord.exception.TripRecordCommentNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TripRecordCommentControllerAdvice { + + @ExceptionHandler(TripRecordCommentNotFoundException.class) + public ResponseEntity> handleTripRecordCommentNotFoundException(TripRecordCommentNotFoundException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/request/TripRecordCommentRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/request/TripRecordCommentRequestDto.java new file mode 100644 index 00000000..81a0a40b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/request/TripRecordCommentRequestDto.java @@ -0,0 +1,39 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.dto.request; + +import com.haejwo.tripcometrue.domain.comment.triprecord.entity.TripRecordComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; + +public record TripRecordCommentRequestDto( + + @NotNull(message = "๋ณธ๋ฌธ์€ ํ•„์ˆ˜๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + @Length(min = 1, max = 500, message = "์ž‘์„ฑ ํ—ˆ์šฉ ๋ฒ”์œ„๋Š” ์ตœ์†Œ 1์ž ๋˜๋Š” ์ตœ๋Œ€ 500์ž ์ž…๋‹ˆ๋‹ค.") + String content + +) { + + public static TripRecordComment toComment(Member member, TripRecord tripRecord, TripRecordCommentRequestDto requestDto) { + return TripRecordComment.builder() + .member(member) + .tripRecord(tripRecord) + .content(requestDto.content) + .build(); + } + + public static TripRecordComment toReplyComment( + Member member, + TripRecord tripRecord, + TripRecordComment tripRecordComment, + TripRecordCommentRequestDto requestDto + ) { + + return TripRecordComment.builder() + .member(member) + .tripRecord(tripRecord) + .parentComment(tripRecordComment) + .content(requestDto.content) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/response/TripRecordCommentListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/response/TripRecordCommentListResponseDto.java new file mode 100644 index 00000000..1b0b2cc8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/response/TripRecordCommentListResponseDto.java @@ -0,0 +1,30 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.dto.response; + +import com.haejwo.tripcometrue.domain.comment.triprecord.entity.TripRecordComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import org.springframework.data.domain.Slice; + +import java.util.List; +import java.util.Objects; + +public record TripRecordCommentListResponseDto( + + int totalCount, + List comments + +) { + + public static TripRecordCommentListResponseDto fromData(int totalCount, Slice tripRecordComments, Member loginMember) { + return new TripRecordCommentListResponseDto( + totalCount, + tripRecordComments.map(tripRecordComment -> { + if (tripRecordComment.getParentComment() == null) { //์ตœ์ƒ์œ„ ๋Œ“๊ธ€๋งŒ ํฌํ•จ + return TripRecordCommentResponseDto.fromEntity(tripRecordComment, loginMember); + } + return null; + }) + .filter(Objects::nonNull) + .toList() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/response/TripRecordCommentResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/response/TripRecordCommentResponseDto.java new file mode 100644 index 00000000..01ea4e73 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/dto/response/TripRecordCommentResponseDto.java @@ -0,0 +1,54 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.comment.triprecord.entity.TripRecordComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +public record TripRecordCommentResponseDto( + + Long commentId, + Long memberId, + String profileUrl, + String nickname, + String content, + boolean isWriter, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt, + + List replyComments + +) { + + public static TripRecordCommentResponseDto fromEntity(TripRecordComment tripRecordComment, Member loginMember) { + return new TripRecordCommentResponseDto( + tripRecordComment.getId(), + tripRecordComment.getMember().getId(), + tripRecordComment.getMember().getProfileImage(), + tripRecordComment.getMember().getMemberBase().getNickname(), + tripRecordComment.getContent(), + isWriter(tripRecordComment, loginMember), + tripRecordComment.getCreatedAt(), + getReplyComments(tripRecordComment, loginMember) //์ž์‹ ๋Œ“๊ธ€ ๋ฆฌ์ŠคํŠธ์— ๋‹ด๊ธฐ + ); + } + + private static boolean isWriter(TripRecordComment tripRecordComment, Member loginMember) { + return Objects.equals(tripRecordComment.getMember(), loginMember); + } + + private static List getReplyComments(TripRecordComment tripRecordComment, Member loginMember) { + if (tripRecordComment.getParentComment() == null) { + return tripRecordComment.getChildComments().stream() + .map(comment -> TripRecordCommentResponseDto.fromEntity(comment, loginMember)) + .sorted(Comparator.comparing(TripRecordCommentResponseDto::createdAt).reversed()) + .toList(); + } + return null; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/entity/TripRecordComment.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/entity/TripRecordComment.java new file mode 100644 index 00000000..ae1afeaf --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/entity/TripRecordComment.java @@ -0,0 +1,53 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.REMOVE; +import static jakarta.persistence.FetchType.LAZY; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordComment extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_comment_id") + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "parent_comment_id") + private TripRecordComment parentComment; + + @OneToMany(mappedBy = "parentComment", cascade = REMOVE, orphanRemoval = true) + private List childComments = new ArrayList<>(); + + @Column(nullable = false) + private String content; + + @Builder + public TripRecordComment(Member member, TripRecord tripRecord, TripRecordComment parentComment, String content) { + this.member = member; + this.tripRecord = tripRecord; + this.parentComment = parentComment; + this.content = content; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/exception/TripRecordCommentNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/exception/TripRecordCommentNotFoundException.java new file mode 100644 index 00000000..25dd855e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/exception/TripRecordCommentNotFoundException.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripRecordCommentNotFoundException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_RECORD_COMMENT_NOT_FOUND; + + public TripRecordCommentNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/repository/TripRecordCommentRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/repository/TripRecordCommentRepository.java new file mode 100644 index 00000000..bf9730aa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/repository/TripRecordCommentRepository.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.repository; + +import com.haejwo.tripcometrue.domain.comment.triprecord.entity.TripRecordComment; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface TripRecordCommentRepository extends JpaRepository { + + Slice findByTripRecord(TripRecord tripRecord, Pageable pageable); + + @Modifying + @Query("delete from TripRecordComment trc where trc.parentComment.id = :tripRecordCommentId") + int deleteChildrenByTripRecordCommentId(@Param("tripRecordCommentId") Long tripRecordCommentId); + + @Modifying + @Query("delete from TripRecordComment trc where trc.id = :tripRecordCommentId") + int deleteParentByTripRecordCommentId(@Param("tripRecordCommentId") Long tripRecordCommentId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/service/TripRecordCommentService.java b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/service/TripRecordCommentService.java new file mode 100644 index 00000000..d5284a4f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/comment/triprecord/service/TripRecordCommentService.java @@ -0,0 +1,134 @@ +package com.haejwo.tripcometrue.domain.comment.triprecord.service; + +import com.haejwo.tripcometrue.domain.alarm.entity.AlarmType; +import com.haejwo.tripcometrue.domain.alarm.service.AlarmService; +import com.haejwo.tripcometrue.domain.comment.triprecord.dto.request.TripRecordCommentRequestDto; +import com.haejwo.tripcometrue.domain.comment.triprecord.dto.response.TripRecordCommentListResponseDto; +import com.haejwo.tripcometrue.domain.comment.triprecord.entity.TripRecordComment; +import com.haejwo.tripcometrue.domain.comment.triprecord.exception.TripRecordCommentNotFoundException; +import com.haejwo.tripcometrue.domain.comment.triprecord.repository.TripRecordCommentRepository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.exception.UserInvalidAccessException; +import com.haejwo.tripcometrue.domain.member.exception.UserNotFoundException; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class TripRecordCommentService { + + private final MemberRepository memberRepository; + private final TripRecordRepository tripRecordRepository; + private final TripRecordCommentRepository tripRecordCommentRepository; + private final AlarmService alarmService; + + public void saveComment( + PrincipalDetails principalDetails, + Long tripRecordId, + TripRecordCommentRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + TripRecord tripRecord = getTripRecordById(tripRecordId); + + TripRecordComment comment = TripRecordCommentRequestDto.toComment(loginMember, tripRecord, requestDto); + tripRecordCommentRepository.save(comment); + tripRecord.incrementCommentCount(); + + alarmService.addAlarm( + loginMember, + tripRecord.getMember(), + AlarmType.NEW_TRIP_RECORD_COMMENT, + tripRecordId, + comment.getId()); + } + + private Member getMember(PrincipalDetails principalDetails) { + if (principalDetails != null) { + return memberRepository.findById(principalDetails.getMember().getId()) + .orElseThrow(UserNotFoundException::new); + } + return null; + } + + public void saveReplyComment( + PrincipalDetails principalDetails, + Long tripRecordCommentId, + TripRecordCommentRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + TripRecordComment tripRecordComment = getTripRecordCommentById(tripRecordCommentId); + TripRecord tripRecord = tripRecordComment.getTripRecord(); + + TripRecordComment comment = TripRecordCommentRequestDto.toReplyComment(loginMember, tripRecord, tripRecordComment, requestDto); + tripRecordCommentRepository.save(comment); + tripRecord.incrementCommentCount(); + } + + private TripRecordComment getTripRecordCommentById(Long tripRecordCommentId) { + return tripRecordCommentRepository.findById(tripRecordCommentId) + .orElseThrow(TripRecordCommentNotFoundException::new); + } + + private TripRecord getTripRecordById(Long tripRecordId) { + return tripRecordRepository.findById(tripRecordId) + .orElseThrow(TripRecordNotFoundException::new); + } + + @Transactional(readOnly = true) + public TripRecordCommentListResponseDto getCommentList( + PrincipalDetails principalDetails, + Long tripRecordId, + Pageable pageable + ) { + + Member loginMember = getMember(principalDetails); + TripRecord tripRecord = getTripRecordById(tripRecordId); + + Slice tripRecordComments = tripRecordCommentRepository.findByTripRecord(tripRecord, pageable); + return TripRecordCommentListResponseDto.fromData(tripRecord.getCommentCount(), tripRecordComments, loginMember); + } + + public void removeComment(PrincipalDetails principalDetails, Long tripRecordCommentId) { + + Member loginMember = getMember(principalDetails); + TripRecordComment tripRecordComment = getTripRecordComment(tripRecordCommentId); + TripRecord tripRecord = tripRecordComment.getTripRecord(); + + validateRightMemberAccess(loginMember, tripRecordComment); + + int removedCount = getRemovedCount(tripRecordCommentId, tripRecordComment); + tripRecord.decreaseCommentCount(removedCount); + } + + private int getRemovedCount(Long tripRecordCommentId, TripRecordComment tripRecordComment) { + int childrenCount = tripRecordCommentRepository.deleteChildrenByTripRecordCommentId(tripRecordComment.getId()); + int parentCount = tripRecordCommentRepository.deleteParentByTripRecordCommentId(tripRecordCommentId); + return childrenCount + parentCount; + } + + private TripRecordComment getTripRecordComment(Long tripRecordCommentId) { + return tripRecordCommentRepository.findById(tripRecordCommentId) + .orElseThrow(TripRecordCommentNotFoundException::new); + } + + private void validateRightMemberAccess(Member member, TripRecordComment tripRecordComment) { + if (!Objects.equals(tripRecordComment.getMember().getId(), member.getId())) { + throw new UserInvalidAccessException(); + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/controller/LikesController.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/controller/LikesController.java new file mode 100644 index 00000000..bd24a6dd --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/controller/LikesController.java @@ -0,0 +1,59 @@ +package com.haejwo.tripcometrue.domain.likes.controller; + +import com.haejwo.tripcometrue.domain.likes.dto.response.PlaceReviewLikesResponseDto; +import com.haejwo.tripcometrue.domain.likes.dto.response.TripRecordReviewLikesResponseDto; +import com.haejwo.tripcometrue.domain.likes.service.LikesService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + + @RestController + @RequiredArgsConstructor + public class LikesController { + + private final LikesService likesService; + + @PostMapping("/v1/places/reviews/{placeReviewId}/likes") + public ResponseEntity> likePlaceReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewId) { + + PlaceReviewLikesResponseDto responseDto = likesService.likePlaceReview(principalDetails, placeReviewId); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @PostMapping("/v1/trip-records/reviews/{tripRecordReviewId}/likes") + public ResponseEntity> likeTripRecordReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordReviewId) { + + TripRecordReviewLikesResponseDto responseDto = likesService.likeTripRecordReview(principalDetails, tripRecordReviewId); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @DeleteMapping("/v1/places/reviews/{placeReviewId}/likes") + public ResponseEntity> unlikePlaceReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewId) { + + likesService.unlikePlaceReview(principalDetails, placeReviewId); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @DeleteMapping("/v1/trip-records/reviews/{tripRecordReviewId}/likes") + public ResponseEntity> unlikeTripRecordReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordReviewId) { + + likesService.unlikeTripRecordReview(principalDetails, tripRecordReviewId); + return ResponseEntity.ok(ResponseDTO.ok()); + } + } + + diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/controller/LikesControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/controller/LikesControllerAdvice.java new file mode 100644 index 00000000..c778fe42 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/controller/LikesControllerAdvice.java @@ -0,0 +1,20 @@ +package com.haejwo.tripcometrue.domain.likes.controller; +import com.haejwo.tripcometrue.domain.likes.exception.InvalidLikesException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class LikesControllerAdvice { + + @ExceptionHandler(InvalidLikesException.class) + public ResponseEntity> handleInvalidLikesException(InvalidLikesException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/dto/response/PlaceReviewLikesResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/dto/response/PlaceReviewLikesResponseDto.java new file mode 100644 index 00000000..a2fca12b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/dto/response/PlaceReviewLikesResponseDto.java @@ -0,0 +1,19 @@ +package com.haejwo.tripcometrue.domain.likes.dto.response; +import com.haejwo.tripcometrue.domain.likes.entity.PlaceReviewLikes; +import lombok.Builder; + + +@Builder +public record PlaceReviewLikesResponseDto( + Long likeId, + Long memberId, + Long placeReviewId +) { + + public static PlaceReviewLikesResponseDto fromEntity(PlaceReviewLikes placeReviewLikes) { + return new PlaceReviewLikesResponseDto( + placeReviewLikes.getId(), + placeReviewLikes.getMember().getId(), + placeReviewLikes.getPlaceReview().getId()); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/dto/response/TripRecordReviewLikesResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/dto/response/TripRecordReviewLikesResponseDto.java new file mode 100644 index 00000000..9058eab4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/dto/response/TripRecordReviewLikesResponseDto.java @@ -0,0 +1,19 @@ +package com.haejwo.tripcometrue.domain.likes.dto.response; +import com.haejwo.tripcometrue.domain.likes.entity.TripRecordReviewLikes; +import lombok.Builder; + +@Builder +public record TripRecordReviewLikesResponseDto( + Long likeId, + Long memberId, + Long tripRecordReviewId +) { + + public static TripRecordReviewLikesResponseDto fromEntity( + TripRecordReviewLikes tripRecordReviewLikes) { + return new TripRecordReviewLikesResponseDto( + tripRecordReviewLikes.getId(), + tripRecordReviewLikes.getMember().getId(), + tripRecordReviewLikes.getTripRecordReview().getId()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/entity/PlaceReviewLikes.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/entity/PlaceReviewLikes.java new file mode 100644 index 00000000..5c24b41d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/entity/PlaceReviewLikes.java @@ -0,0 +1,47 @@ +package com.haejwo.tripcometrue.domain.likes.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PlaceReviewLikes { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "place_review_likes_id") + private Long Id; + + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "place_review_id") + private PlaceReview placeReview; + + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "member_id") + private Member member; + + + @Builder + public PlaceReviewLikes(Member member, PlaceReview placeReview) { + this.member = member; + this.placeReview = placeReview; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/entity/TripRecordReviewLikes.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/entity/TripRecordReviewLikes.java new file mode 100644 index 00000000..f0d98dc5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/entity/TripRecordReviewLikes.java @@ -0,0 +1,49 @@ +package com.haejwo.tripcometrue.domain.likes.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordReviewLikes { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_review_likes_id") + private Long Id; + + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "trip_record_review_id") + private TripRecordReview tripRecordReview; + + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "member_id") + private Member member; + + + @Builder + public TripRecordReviewLikes(Member member, TripRecordReview tripRecordReview) { + this.member = member; + this.tripRecordReview = tripRecordReview; + } + + + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/exception/InvalidLikesException.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/exception/InvalidLikesException.java new file mode 100644 index 00000000..2fc52345 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/exception/InvalidLikesException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.likes.exception; +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class InvalidLikesException extends ApplicationException { + + public InvalidLikesException(ErrorCode errorCode){ + super(errorCode); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/repository/PlaceReviewLikesRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/repository/PlaceReviewLikesRepository.java new file mode 100644 index 00000000..13d3333a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/repository/PlaceReviewLikesRepository.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.likes.repository; +import com.haejwo.tripcometrue.domain.likes.entity.PlaceReviewLikes; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PlaceReviewLikesRepository extends JpaRepository { + + Optional findByMemberIdAndPlaceReviewId(Long memberId, Long placeReviewId); + boolean existsByMemberAndPlaceReview(Member member, PlaceReview placeReview); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/repository/TripRecordReviewLikesRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/repository/TripRecordReviewLikesRepository.java new file mode 100644 index 00000000..f7525551 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/repository/TripRecordReviewLikesRepository.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.likes.repository; +import com.haejwo.tripcometrue.domain.likes.entity.TripRecordReviewLikes; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TripRecordReviewLikesRepository extends JpaRepository { + + Optional findByMemberIdAndTripRecordReviewId(Long memberId, Long tripRecordReviewId); + public boolean existsByMemberAndTripRecordReview(Member member, TripRecordReview tripRecordReview); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/likes/service/LikesService.java b/src/main/java/com/haejwo/tripcometrue/domain/likes/service/LikesService.java new file mode 100644 index 00000000..6fcadd38 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/likes/service/LikesService.java @@ -0,0 +1,118 @@ +package com.haejwo.tripcometrue.domain.likes.service; + +import com.haejwo.tripcometrue.domain.likes.dto.response.PlaceReviewLikesResponseDto; +import com.haejwo.tripcometrue.domain.likes.dto.response.TripRecordReviewLikesResponseDto; +import com.haejwo.tripcometrue.domain.likes.entity.PlaceReviewLikes; +import com.haejwo.tripcometrue.domain.likes.entity.TripRecordReviewLikes; +import com.haejwo.tripcometrue.domain.likes.exception.InvalidLikesException; +import com.haejwo.tripcometrue.domain.likes.repository.PlaceReviewLikesRepository; +import com.haejwo.tripcometrue.domain.likes.repository.TripRecordReviewLikesRepository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewNotFoundException; +import com.haejwo.tripcometrue.domain.review.placereview.repository.PlaceReviewRepository; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import com.haejwo.tripcometrue.domain.review.triprecordreview.exception.TripRecordReviewNotFoundException; +import com.haejwo.tripcometrue.domain.review.triprecordreview.repository.TripRecordReviewRepository; +import com.haejwo.tripcometrue.global.exception.ErrorCode; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import jakarta.transaction.Transactional; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + + @Service + @RequiredArgsConstructor + public class LikesService { + + private final MemberRepository memberRepository; + private final PlaceReviewRepository placeReviewRepository; + private final TripRecordReviewRepository tripRecordReviewRepository; + private final PlaceReviewLikesRepository placeReviewLikesRepository; + private final TripRecordReviewLikesRepository tripRecordReviewLikesRepository; + + @Transactional + public PlaceReviewLikesResponseDto likePlaceReview(PrincipalDetails principalDetails, Long placeReviewId) { + Long memberId = principalDetails.getMember().getId(); + + Optional existingLikes = placeReviewLikesRepository.findByMemberIdAndPlaceReviewId(memberId, placeReviewId); + if (existingLikes.isPresent()) { + throw new InvalidLikesException(ErrorCode.LIKES_ALREADY_EXIST); + } + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new InvalidLikesException(ErrorCode.USER_NOT_FOUND)); + PlaceReview placeReview = placeReviewRepository.findById(placeReviewId) + .orElseThrow(() -> new InvalidLikesException(ErrorCode.PLACE_REVIEW_NOT_FOUND)); + + PlaceReviewLikes like = PlaceReviewLikes.builder() + .member(member) + .placeReview(placeReview) + .build(); + placeReviewLikesRepository.save(like); + placeReview.increaseLikesCount(); + + return PlaceReviewLikesResponseDto.fromEntity(like); + } + + @Transactional + public TripRecordReviewLikesResponseDto likeTripRecordReview(PrincipalDetails principalDetails, Long tripRecordReviewId) { + Long memberId = principalDetails.getMember().getId(); + + Optional existingLikes = tripRecordReviewLikesRepository.findByMemberIdAndTripRecordReviewId(memberId, tripRecordReviewId); + if (existingLikes.isPresent()) { + throw new InvalidLikesException(ErrorCode.LIKES_ALREADY_EXIST); + } + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new InvalidLikesException(ErrorCode.USER_NOT_FOUND)); + TripRecordReview tripRecordReview = tripRecordReviewRepository.findById(tripRecordReviewId) + .orElseThrow(() -> new InvalidLikesException(ErrorCode.TRIP_RECORD_REVIEW_NOT_FOUND)); + + TripRecordReviewLikes like = TripRecordReviewLikes.builder() + .member(member) + .tripRecordReview(tripRecordReview) + .build(); + tripRecordReviewLikesRepository.save(like); + tripRecordReview.increaseLikesCount(); + + return TripRecordReviewLikesResponseDto.fromEntity(like); + } + + @Transactional + public void unlikePlaceReview(PrincipalDetails principalDetails, Long placeReviewId) { + Long memberId = principalDetails.getMember().getId(); + PlaceReview placeReview = findByPlaceReviewId(placeReviewId); + + PlaceReviewLikes like = placeReviewLikesRepository.findByMemberIdAndPlaceReviewId(memberId, placeReviewId) + .orElseThrow(() -> new InvalidLikesException(ErrorCode.LIKES_NOT_FOUND)); + + placeReviewLikesRepository.delete(like); + placeReview.decreaseLikesCount(); + } + + private PlaceReview findByPlaceReviewId(Long placeReviewId) { + return placeReviewRepository.findById(placeReviewId) + .orElseThrow(PlaceReviewNotFoundException::new); + } + + @Transactional + public void unlikeTripRecordReview(PrincipalDetails principalDetails, Long tripRecordReviewId) { + Long memberId = principalDetails.getMember().getId(); + TripRecordReview tripRecordReview = findByTripRecordReviewId(tripRecordReviewId); + + TripRecordReviewLikes like = tripRecordReviewLikesRepository.findByMemberIdAndTripRecordReviewId(memberId, tripRecordReviewId) + .orElseThrow(() -> new InvalidLikesException(ErrorCode.LIKES_NOT_FOUND)); + + tripRecordReviewLikesRepository.delete(like); + tripRecordReview.decreaseLikesCount(); + } + + private TripRecordReview findByTripRecordReviewId(Long tripRecordReviewId) { + return tripRecordReviewRepository.findById(tripRecordReviewId) + .orElseThrow(TripRecordReviewNotFoundException::new); + } + } + + diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java index 42033e55..1ceae609 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberController.java @@ -1,15 +1,32 @@ package com.haejwo.tripcometrue.domain.member.controller; -import com.haejwo.tripcometrue.domain.member.request.SignUpRequest; -import com.haejwo.tripcometrue.domain.member.response.SignUpResponse; +import com.haejwo.tripcometrue.domain.member.dto.request.IntroductionRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.NicknameRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.PasswordRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.ProfileImageRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.SignUpRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.response.IntroductionResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.LoginResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.MemberDetailResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.NicknameResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.ProfileImageResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.SignUpResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.TestUserResponseDto; +import com.haejwo.tripcometrue.domain.member.entity.Member; import com.haejwo.tripcometrue.domain.member.service.MemberService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; import com.haejwo.tripcometrue.global.util.ResponseDTO; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -20,9 +37,125 @@ public class MemberController { private final MemberService memberService; @PostMapping("/signup") - public ResponseEntity> signup( - @Valid @RequestBody SignUpRequest signUpRequest) { - ResponseDTO response = memberService.signup(signUpRequest); - return ResponseEntity.status(response.getCode()).body(response); + public ResponseEntity> signup( + @Valid @RequestBody SignUpRequestDto signUpRequestDto) { + SignUpResponseDto signUpResponseDto = memberService.signup(signUpRequestDto); + ResponseDTO response = ResponseDTO.okWithData(signUpResponseDto); + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + // Authenticated user ์ƒ˜ํ”Œํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค + @GetMapping("/test/jwt") + public ResponseEntity> test( + @AuthenticationPrincipal PrincipalDetails principalDetails) { + Member member = principalDetails.getMember(); + + TestUserResponseDto testUserResponseDto = TestUserResponseDto.fromEntity(member); + ResponseDTO response = ResponseDTO.okWithData(testUserResponseDto); + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @GetMapping("/check-duplicated-email") + public ResponseEntity> checkDuplicateEmail( + @RequestParam String email) { + memberService.checkDuplicateEmail(email); + ResponseDTO response = ResponseDTO.ok(); + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @GetMapping("/oauth2/info") + public ResponseEntity> oauth2Test( + @RequestParam String token, @RequestParam String email, @RequestParam String name) { + LoginResponseDto loginResponseDto = new LoginResponseDto(email, name, token); + + ResponseDTO response = ResponseDTO.okWithData(loginResponseDto); + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @PatchMapping("/change-password") + public ResponseEntity> changePassword( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @Valid @RequestBody PasswordRequestDto passwordRequestDto) { + + memberService.changePassword(principalDetails, passwordRequestDto); + ResponseDTO response = ResponseDTO.ok(); + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @PostMapping("/check-password") + public ResponseEntity> checkPassword( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @Valid @RequestBody PasswordRequestDto passwordRequestDto) { + + memberService.checkPassword(principalDetails, passwordRequestDto); + ResponseDTO response = ResponseDTO.ok(); + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @PatchMapping("/profile-image") + public ResponseEntity> updateProfileImage( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @Valid @RequestBody ProfileImageRequestDto requestDto) { + + ProfileImageResponseDto responseDto = memberService.updateProfileImage(principalDetails, requestDto); + ResponseDTO response = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @PatchMapping("/introduction") + public ResponseEntity> updateIntroduction( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @Valid @RequestBody IntroductionRequestDto requestDto) { + + IntroductionResponseDto responseDto = memberService.updateIntroduction(principalDetails, requestDto); + ResponseDTO response = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @PatchMapping("/nickname") + public ResponseEntity> updateNickname( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @Valid @RequestBody NicknameRequestDto requestDto) { + + NicknameResponseDto responseDto = memberService.updateNickname(principalDetails, requestDto); + ResponseDTO response = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(response.getCode()) + .body(response); + } + + @DeleteMapping + public ResponseEntity> deleteAccount(@AuthenticationPrincipal PrincipalDetails principalDetails) { + memberService.deleteAccount(principalDetails); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @GetMapping("/details") + public ResponseEntity> getMemberDetails( + @AuthenticationPrincipal PrincipalDetails principalDetails) { + MemberDetailResponseDto responseDto = memberService.getMemberDetails(principalDetails); + ResponseDTO response = ResponseDTO.okWithData(responseDto); + return ResponseEntity + .status(response.getCode()) + .body(response); } } \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberControllerAdvice.java index 7b213e3d..31420096 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberControllerAdvice.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberControllerAdvice.java @@ -1,6 +1,14 @@ package com.haejwo.tripcometrue.domain.member.controller; +import com.haejwo.tripcometrue.domain.member.exception.CurrentPasswordNotMatchException; import com.haejwo.tripcometrue.domain.member.exception.EmailDuplicateException; +import com.haejwo.tripcometrue.domain.member.exception.EmailNotMatchException; +import com.haejwo.tripcometrue.domain.member.exception.IntroductionLengthExceededException; +import com.haejwo.tripcometrue.domain.member.exception.NewPasswordNotMatchException; +import com.haejwo.tripcometrue.domain.member.exception.NewPasswordSameAsOldException; +import com.haejwo.tripcometrue.domain.member.exception.UserInvalidAccessException; +import com.haejwo.tripcometrue.domain.member.exception.*; +import com.haejwo.tripcometrue.global.exception.ApplicationException; import com.haejwo.tripcometrue.global.util.ResponseDTO; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -23,5 +31,44 @@ public ResponseEntity> handleUserControllerExceptions( return ResponseEntity.status(status).body( ResponseDTO.errorWithMessage(status, exception.getMessage())); } + + @ExceptionHandler(UserInvalidAccessException.class) + public ResponseEntity> userInvalidAccessException( + UserInvalidAccessException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler({CurrentPasswordNotMatchException.class, NewPasswordSameAsOldException.class, NewPasswordNotMatchException.class, + EmailNotMatchException.class}) + public ResponseEntity> handleApplicationException(ApplicationException exception) { + HttpStatus status = exception.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, exception.getMessage())); + } + + @ExceptionHandler(IntroductionLengthExceededException.class) + public ResponseEntity> handleIntroductionLengthExceededException(IntroductionLengthExceededException exception) { + HttpStatus status = exception.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, exception.getMessage())); + } + + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity> handleUserNotFoundException(UserNotFoundException exception) { + HttpStatus status = exception.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, exception.getMessage())); + } } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberReadSearchController.java b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberReadSearchController.java new file mode 100644 index 00000000..49d77f52 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/controller/MemberReadSearchController.java @@ -0,0 +1,74 @@ +package com.haejwo.tripcometrue.domain.member.controller; + +import com.haejwo.tripcometrue.domain.member.dto.response.MemberCreatorInfoResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.MemberDetailListItemResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.MemberSearchResultWithContentResponseDto; +import com.haejwo.tripcometrue.domain.member.facade.MemberReadSearchFacade; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RequestMapping("/v1/members") +@RestController +public class MemberReadSearchController { + + private final MemberReadSearchFacade memberReadSearchFacade; + + @GetMapping("/{memberId}") + public ResponseEntity> memberCreatorInfo( + @PathVariable("memberId") Long memberId + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + memberReadSearchFacade.getCreatorInfo(memberId) + ) + ); + } + + @GetMapping("/list") + public ResponseEntity> searchByNicknameResultWithContent( + @RequestParam("query") String query + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + memberReadSearchFacade.searchByNicknameResultWithContent(query) + ) + ); + } + + @GetMapping + public ResponseEntity>> searchByNicknamePagination( + @RequestParam("query") String query, + @PageableDefault(sort = "rating", direction = Sort.Direction.DESC) Pageable pageable + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + memberReadSearchFacade.searchByNicknamePagination(query, pageable) + ) + ); + } + + // ํ™ˆ ํ”ผ๋“œ HOT ์ธ๊ธฐ ํฌ๋ฆฌ์—์ดํ„ฐ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @GetMapping("/top-list") + public ResponseEntity> listTopCreators() { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + memberReadSearchFacade.listTopMemberCreators() + ) + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/EmailCheckRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/EmailCheckRequestDto.java new file mode 100644 index 00000000..97213a87 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/EmailCheckRequestDto.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.member.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record EmailCheckRequestDto( + + @NotBlank(message = "์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.") + @Email(message = "์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.") + String email + +) {} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/IntroductionRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/IntroductionRequestDto.java new file mode 100644 index 00000000..f2611c39 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/IntroductionRequestDto.java @@ -0,0 +1,7 @@ +package com.haejwo.tripcometrue.domain.member.dto.request; + +import jakarta.validation.constraints.Size; + +public record IntroductionRequestDto( + String introduction) +{ } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/LoginRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/LoginRequestDto.java new file mode 100644 index 00000000..88098c05 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/LoginRequestDto.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.member.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record LoginRequestDto( + @NotNull(message = "email์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String email, + @NotNull(message = "password์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String password +) { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/NicknameRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/NicknameRequestDto.java new file mode 100644 index 00000000..24974fde --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/NicknameRequestDto.java @@ -0,0 +1,8 @@ +package com.haejwo.tripcometrue.domain.member.dto.request; + +import com.haejwo.tripcometrue.domain.member.dto.response.NicknameResponseDto; +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record NicknameRequestDto( + String nickname +) {} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/PasswordRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/PasswordRequestDto.java new file mode 100644 index 00000000..c2f23b44 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/PasswordRequestDto.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.member.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record PasswordRequestDto( + String currentPassword, + + String newPassword, + + String confirmPassword +) {} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/ProfileImageRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/ProfileImageRequestDto.java new file mode 100644 index 00000000..66c52edf --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/ProfileImageRequestDto.java @@ -0,0 +1,5 @@ +package com.haejwo.tripcometrue.domain.member.dto.request; + +public record ProfileImageRequestDto( + String profile_image +) {} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/request/SignUpRequest.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/SignUpRequestDto.java similarity index 71% rename from src/main/java/com/haejwo/tripcometrue/domain/member/request/SignUpRequest.java rename to src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/SignUpRequestDto.java index e65d952d..66c32663 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/request/SignUpRequest.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/request/SignUpRequestDto.java @@ -1,25 +1,23 @@ -package com.haejwo.tripcometrue.domain.member.request; +package com.haejwo.tripcometrue.domain.member.dto.request; import com.haejwo.tripcometrue.domain.member.entity.Member; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; -public record SignUpRequest( + +public record SignUpRequestDto( @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "์ด๋ฉ”์ผ ํ˜•์‹์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") @NotNull(message = "email์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") String email, - @NotNull(message = "nickname์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") - String nickname, - @NotNull(message = "password๋Š” ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") String password ) { - public Member toEntity(String encodedPassword) { + public Member toEntity(String encodedPassword, String name) { return Member.builder() .email(email) - .nickname(nickname) + .nickname(name) .password(encodedPassword) .authority("ROLE_USER") .build(); diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/IntroductionResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/IntroductionResponseDto.java new file mode 100644 index 00000000..b65ccfcd --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/IntroductionResponseDto.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record IntroductionResponseDto(String introduction) { + public static IntroductionResponseDto fromEntity(Member member) { + return new IntroductionResponseDto(member.getIntroduction()); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/LoginResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/LoginResponseDto.java new file mode 100644 index 00000000..82a053d3 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/LoginResponseDto.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record LoginResponseDto( + String email, + String name, + String token +) { + + public static LoginResponseDto fromEntity(Member member, String token) { + return new LoginResponseDto( + member.getMemberBase().getEmail(), + member.getMemberBase().getNickname(), + token + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberCreatorInfoResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberCreatorInfoResponseDto.java new file mode 100644 index 00000000..71932284 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberCreatorInfoResponseDto.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import lombok.Builder; + +import java.util.List; + +public record MemberCreatorInfoResponseDto( + MemberDetailListItemResponseDto memberDetailInfo, + List videos, + List tripRecords +) { + + @Builder + public MemberCreatorInfoResponseDto { + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberDetailListItemResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberDetailListItemResponseDto.java new file mode 100644 index 00000000..f60c6e74 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberDetailListItemResponseDto.java @@ -0,0 +1,30 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import lombok.Builder; + +public record MemberDetailListItemResponseDto( + Long memberId, + String nickname, + String introduction, + String profileImageUrl, + Double averageRating, + Integer tripRecordTotal, + Integer videoTotal +) { + + @Builder + public MemberDetailListItemResponseDto { + } + + public static MemberDetailListItemResponseDto of(MemberSimpleResponseDto dto, Integer tripRecordTotal, Integer videoTotal) { + return MemberDetailListItemResponseDto.builder() + .memberId(dto.memberId()) + .nickname(dto.nickname()) + .introduction(dto.introduction()) + .profileImageUrl(dto.profileImageUrl()) + .averageRating(dto.averageRating()) + .tripRecordTotal(tripRecordTotal) + .videoTotal(videoTotal) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberDetailResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberDetailResponseDto.java new file mode 100644 index 00000000..40dc57d2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberDetailResponseDto.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record MemberDetailResponseDto( + Long id, + String nickname, + String profileImage, + String introduction, + Integer totalPoint, + String tripLevel +) { + public static MemberDetailResponseDto fromEntity(Member member) { + return new MemberDetailResponseDto( + member.getId(), + member.getMemberBase().getNickname(), + member.getProfileImage(), + member.getIntroduction(), + member.getTotalPoint(), + member.getTripLevel().name() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberInfoWithTripRecordsResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberInfoWithTripRecordsResponseDto.java new file mode 100644 index 00000000..f7097af7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberInfoWithTripRecordsResponseDto.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import lombok.Builder; + +import java.util.List; + +public record MemberInfoWithTripRecordsResponseDto( + MemberSimpleResponseDto memberInfo, + List tripRecords +) { + + @Builder + public MemberInfoWithTripRecordsResponseDto { + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberSearchResultWithContentResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberSearchResultWithContentResponseDto.java new file mode 100644 index 00000000..98ffb6bb --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberSearchResultWithContentResponseDto.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import lombok.Builder; + +import java.util.List; + +public record MemberSearchResultWithContentResponseDto( + List members, + List videos, + List tripRecords +) { + + @Builder + public MemberSearchResultWithContentResponseDto { + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberSimpleResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberSimpleResponseDto.java new file mode 100644 index 00000000..a07015aa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/MemberSimpleResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import lombok.Builder; + +public record MemberSimpleResponseDto( + Long memberId, + String nickname, + String introduction, + String profileImageUrl, + Double averageRating +) { + + @Builder + public MemberSimpleResponseDto { + } + + public static MemberSimpleResponseDto fromEntity(Member entity) { + return MemberSimpleResponseDto.builder() + .memberId(entity.getId()) + .nickname(entity.getMemberBase().getNickname()) + .introduction(entity.getIntroduction()) + .profileImageUrl(entity.getProfileImage()) + .averageRating(entity.getMemberRating()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/NicknameResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/NicknameResponseDto.java new file mode 100644 index 00000000..561ded5a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/NicknameResponseDto.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record NicknameResponseDto(String nickname) { + public static NicknameResponseDto fromEntity(Member member) { + return new NicknameResponseDto(member.getMemberBase().getNickname()); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/ProfileImageResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/ProfileImageResponseDto.java new file mode 100644 index 00000000..9dfd77c1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/ProfileImageResponseDto.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record ProfileImageResponseDto(String profileImageUrl) { + public static ProfileImageResponseDto fromEntity(Member member) { + return new ProfileImageResponseDto(member.getProfileImage()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/response/SignUpResponse.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/SignUpResponseDto.java similarity index 51% rename from src/main/java/com/haejwo/tripcometrue/domain/member/response/SignUpResponse.java rename to src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/SignUpResponseDto.java index b0fd5e21..d03ea4dd 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/response/SignUpResponse.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/SignUpResponseDto.java @@ -1,18 +1,19 @@ -package com.haejwo.tripcometrue.domain.member.response; +package com.haejwo.tripcometrue.domain.member.dto.response; import com.haejwo.tripcometrue.domain.member.entity.Member; -public record SignUpResponse( +public record SignUpResponseDto( Long memberId, String email, String name ) { - public static SignUpResponse fromEntity(Member member) { - return new SignUpResponse ( - member.getMemberId(), + + public static SignUpResponseDto fromEntity(Member member) { + return new SignUpResponseDto( + member.getId(), member.getMemberBase().getEmail(), member.getMemberBase().getNickname() ); diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/TestUserResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/TestUserResponseDto.java new file mode 100644 index 00000000..3ca11d7c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/dto/response/TestUserResponseDto.java @@ -0,0 +1,22 @@ +package com.haejwo.tripcometrue.domain.member.dto.response; + +import com.haejwo.tripcometrue.domain.member.entity.Member; + +public record TestUserResponseDto( + + String email, + String nickname, + String authority, + String provider + +) { + + public static TestUserResponseDto fromEntity(Member member) { + return new TestUserResponseDto( + member.getMemberBase().getEmail(), + member.getMemberBase().getNickname(), + member.getMemberBase().getAuthority(), + member.getProvider() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java index 69c9f41c..73c50ea4 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/Member.java @@ -1,16 +1,24 @@ package com.haejwo.tripcometrue.domain.member.entity; +import com.haejwo.tripcometrue.domain.member.entity.tripLevel.TripLevel; import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.Objects; + @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @@ -18,21 +26,69 @@ public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long memberId; + @Column(name = "member_id") + private Long id; @Embedded protected MemberBase memberBase; private String provider; - private String profile_image; + private String profileImage; + + private Integer totalPoint; + + @Enumerated(EnumType.STRING) + private TripLevel tripLevel; + + private String introduction; + + private Integer nickNameChangeCount; - private Integer total_point; + private Double memberRating; - private Double member_rating; + private LocalDateTime nickNameChangeTime; @Builder - public Member(String email, String nickname, String password, String authority) { + public Member(String email, String nickname, String password, String authority, + String provider, Double memberRating) { this.memberBase = new MemberBase(email, nickname, password, authority); + this.provider = provider; + this.memberRating = Objects.isNull(memberRating) ? 0.0 : memberRating; + } + + public void updateProfileImage(String profileImage){ + this.profileImage = profileImage; + } + + public void updateIntroduction(String introduction){ + this.introduction = introduction; + } + + public void updateNickNameChangeCount(){ + this.nickNameChangeCount = (this.nickNameChangeCount == null) ? 1 : this.nickNameChangeCount + 1; + } + + public void updateNickNameChangeTime(LocalDateTime nicknameChangeTime){ + this.nickNameChangeTime = nicknameChangeTime; + } + + public void updateTripLevel(){ + this.tripLevel = TripLevel.getLevelByPoint(this.totalPoint); + } + + public void earnPoint(int point) { + this.totalPoint += point; + updateTripLevel(); + } + + @PrePersist + private void init(){ + totalPoint = (totalPoint == null) ? 0 : totalPoint; + nickNameChangeCount = (nickNameChangeCount == null) ? 0 : nickNameChangeCount; + tripLevel = (tripLevel == null) ? TripLevel.BEGINNER : tripLevel; + profileImage = (profileImage == null) ? "https://i.imgur.com/PWZeQcP.png" : profileImage; //์ž„์‹œ ๋””ํดํŠธํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + + updateTripLevel(); } } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/MemberBase.java b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/MemberBase.java index a5156c7d..70298bd2 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/MemberBase.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/MemberBase.java @@ -11,8 +11,15 @@ @AllArgsConstructor @Embeddable public class MemberBase { - private String email; - private String nickname; - private String password; - private String authority; + + private String email; + private String nickname; + private String password; + private String authority; + + public void changePassword(String encodedNewPassword) { + this.password = encodedNewPassword; + } + + public void changeNickname(String nickname){this.nickname = nickname;} } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/entity/tripLevel/TripLevel.java b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/tripLevel/TripLevel.java new file mode 100644 index 00000000..4659cc8e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/entity/tripLevel/TripLevel.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.member.entity.tripLevel; + +public enum TripLevel { + BEGINNER(0, 49), + RUNNER(50, 149), + TRAVELER(150, Integer.MAX_VALUE); + + private final int min; + private final int max; + + TripLevel(int min, int max) { + this.min = min; + this.max = max; + } + + public static TripLevel getLevelByPoint(int totalPoints) { + for (TripLevel level : TripLevel.values()) { + if (totalPoints >= level.min && totalPoints <= level.max) { + return level; + } + } + return TRAVELER; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/CurrentPasswordNotMatchException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/CurrentPasswordNotMatchException.java new file mode 100644 index 00000000..a8814156 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/CurrentPasswordNotMatchException.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class CurrentPasswordNotMatchException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.CURRENT_PASSWORD_NOT_MATCH; + + public CurrentPasswordNotMatchException(){super(ERROR_CODE);} +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/EmailNotMatchException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/EmailNotMatchException.java new file mode 100644 index 00000000..ade336f8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/EmailNotMatchException.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class EmailNotMatchException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.EMAIL_NOT_MATCH; + + public EmailNotMatchException(){super(ERROR_CODE);} +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/IntroductionLengthExceededException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/IntroductionLengthExceededException.java new file mode 100644 index 00000000..6a115909 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/IntroductionLengthExceededException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class IntroductionLengthExceededException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.INTRODUCTION_TOO_LONG; + + public IntroductionLengthExceededException(){super(ERROR_CODE);} + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NewPasswordNotMatchException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NewPasswordNotMatchException.java new file mode 100644 index 00000000..5921aaef --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NewPasswordNotMatchException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class NewPasswordNotMatchException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.NEW_PASSWORD_NOT_MATCH; + + public NewPasswordNotMatchException(){super(ERROR_CODE);} + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NewPasswordSameAsOldException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NewPasswordSameAsOldException.java new file mode 100644 index 00000000..00832eee --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NewPasswordSameAsOldException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class NewPasswordSameAsOldException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.NEW_PASSWORD_SAME_AS_OLD; + + public NewPasswordSameAsOldException(){super(ERROR_CODE);} +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NicknameAlreadyExistsException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NicknameAlreadyExistsException.java new file mode 100644 index 00000000..b851e709 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NicknameAlreadyExistsException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class NicknameAlreadyExistsException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.NICKNAME_ALREADY_EXISTS; + + public NicknameAlreadyExistsException(){super(ERROR_CODE);} +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NicknameChangeNotAvailableException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NicknameChangeNotAvailableException.java new file mode 100644 index 00000000..ea061182 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/NicknameChangeNotAvailableException.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.member.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class NicknameChangeNotAvailableException extends ApplicationException { + + private static ErrorCode ERROR_CODE = ErrorCode.NICKNAME_CHANGE_NOT_AVAILABLE; + + public NicknameChangeNotAvailableException(){ + super(ERROR_CODE); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/UserInvalidAccessException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/UserInvalidAccessException.java new file mode 100644 index 00000000..1c155c68 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/UserInvalidAccessException.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.member.exception; + + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class UserInvalidAccessException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.USER_INVALID_ACCESS; + + public UserInvalidAccessException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/exception/UserNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/UserNotFoundException.java new file mode 100644 index 00000000..94a01b8b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/exception/UserNotFoundException.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.member.exception; + + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class UserNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.USER_NOT_FOUND; + + public UserNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/facade/MemberReadSearchFacade.java b/src/main/java/com/haejwo/tripcometrue/domain/member/facade/MemberReadSearchFacade.java new file mode 100644 index 00000000..9d156d4a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/facade/MemberReadSearchFacade.java @@ -0,0 +1,146 @@ +package com.haejwo.tripcometrue.domain.member.facade; + +import com.haejwo.tripcometrue.domain.member.dto.response.*; +import com.haejwo.tripcometrue.domain.member.service.MemberReadSearchService; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.service.TripRecordScheduleVideoService; +import com.haejwo.tripcometrue.domain.triprecord.service.TripRecordService; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +@Service +public class MemberReadSearchFacade { + + private final MemberReadSearchService memberReadSearchService; + private final TripRecordScheduleVideoService tripRecordScheduleVideoService; + private final TripRecordService tripRecordService; + + public MemberSearchResultWithContentResponseDto searchByNicknameResultWithContent(String nickname) { + List members = memberReadSearchService.searchByNickname(nickname); + + List memberIds = members + .stream() + .map(MemberSimpleResponseDto::memberId) + .toList(); + + List videos = tripRecordScheduleVideoService.getVideosInMemberIds(memberIds); + List tripRecords = tripRecordService.findTripRecordsWihMemberInMemberIds(memberIds); + + return MemberSearchResultWithContentResponseDto.builder() + .members(members) + .videos(videos) + .tripRecords(tripRecords) + .build(); + } + + public SliceResponseDto searchByNicknamePagination(String query, Pageable pageable) { + + SliceResponseDto sliceResult = memberReadSearchService.searchByNickname(query, pageable); + + List memberIds = sliceResult.content() + .stream() + .map(MemberSimpleResponseDto::memberId) + .toList(); + + Map> tripRecordMap = getGroupByMemberIdTripRecordMap( + tripRecordService.findTripRecordsWihMemberInMemberIds(memberIds) + ); + + Map> videoMap = getGroupByMemberIdVideoMap( + tripRecordScheduleVideoService.getVideosInMemberIds(memberIds) + ); + + + return SliceResponseDto.builder() + .content( + sliceResult.content() + .stream() + .map(dto -> { + int tripRecordTotal = (Objects.isNull(tripRecordMap.get(dto.memberId()))) ? 0 : tripRecordMap.get(dto.memberId()).size(); + int videoTotal = (Objects.isNull(videoMap.get(dto.memberId()))) ? 0 : videoMap.get(dto.memberId()).size(); + + return MemberDetailListItemResponseDto.of(dto, tripRecordTotal, videoTotal); + } + ) + .toList() + ) + .totalCount(sliceResult.totalCount()) + .currentPageNum(sliceResult.currentPageNum()) + .pageSize(sliceResult.pageSize()) + .sort(sliceResult.sort()) + .first(sliceResult.first()) + .last(sliceResult.last()) + .build(); + } + + public MemberCreatorInfoResponseDto getCreatorInfo(Long memberId) { + MemberSimpleResponseDto memberInfo = memberReadSearchService.getMemberSimpleInfo(memberId); + + List videos = tripRecordScheduleVideoService.getVideosInMemberIds(List.of(memberId)); + List tripRecords = tripRecordService.findTripRecordsWihMemberInMemberIds(List.of(memberId)); + + return MemberCreatorInfoResponseDto.builder() + .memberDetailInfo( + MemberDetailListItemResponseDto.of( + memberInfo, tripRecords.size(), videos.size() + ) + ) + .videos(videos) + .tripRecords(tripRecords) + .build(); + } + + public List listTopMemberCreators() { + List memberSimpleInfos = memberReadSearchService.listTopMemberSimpleInfos(); + log.info("memberSimpleInfo"); + + + Map> tripRecordMap = getGroupByMemberIdTripRecordMap( + tripRecordService.findTripRecordsWihMemberInMemberIds( + memberSimpleInfos + .stream() + .map(MemberSimpleResponseDto::memberId) + .toList() + ) + ); + + return memberSimpleInfos.stream() + .map(memberInfo -> MemberInfoWithTripRecordsResponseDto.builder() + .memberInfo(memberInfo) + .tripRecords( + Objects.nonNull(tripRecordMap.get(memberInfo.memberId())) ? + tripRecordMap.get(memberInfo.memberId()) : new ArrayList<>() + ) + .build() + ) + .toList(); + } + + private Map> getGroupByMemberIdVideoMap( + List tripRecordScheduleVideos + ) { + return tripRecordScheduleVideos + .stream() + .collect(Collectors.groupingBy(TripRecordScheduleVideoListItemResponseDto::memberId)); + } + + private Map> getGroupByMemberIdTripRecordMap( + List tripRecords + ) { + return tripRecords + .stream() + .collect(Collectors.groupingBy(TripRecordListItemResponseDto::memberId)); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java index d762be18..00f91e11 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepository.java @@ -4,8 +4,12 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface MemberRepository extends JpaRepository { +public interface MemberRepository + extends JpaRepository, MemberRepositoryCustom { Optional findByMemberBaseEmail(String email); + Optional findByMemberBaseEmailAndProvider(String email, String provider); + + Optional findByMemberBaseNickname(String nickname); } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepositoryCustom.java new file mode 100644 index 00000000..daca45af --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepositoryCustom.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.member.repository; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public interface MemberRepositoryCustom { + + List findByNicknameOrderByMemberRating(String nickname); + + Slice findByNicknameOrderByMemberRating(String nickname, Pageable pageable); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepositoryImpl.java new file mode 100644 index 00000000..ac2f2d2a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/repository/MemberRepositoryImpl.java @@ -0,0 +1,68 @@ +package com.haejwo.tripcometrue.domain.member.repository; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.util.StringUtils; + +import java.util.List; + +import static com.haejwo.tripcometrue.domain.member.entity.QMember.member; + +@RequiredArgsConstructor +public class MemberRepositoryImpl implements MemberRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findByNicknameOrderByMemberRating(String nickname) { + return queryFactory + .selectFrom(member) + .where( + containsIgnoreCaseNickname(nickname) + ) + .orderBy(member.memberRating.desc()) + .fetch(); + } + + @Override + public Slice findByNicknameOrderByMemberRating(String nickname, Pageable pageable) { + + int pageSize = pageable.getPageSize(); + + List content = queryFactory + .selectFrom(member) + .where( + containsIgnoreCaseNickname(nickname) + ) + .orderBy(member.memberRating.desc()) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + private BooleanExpression containsIgnoreCaseNickname(String nickname) { + + if (!StringUtils.hasText(nickname)) { + return null; + } + + String replacedNickname = nickname.replaceAll(" ", ""); + + return Expressions.stringTemplate("function('replace',{0},{1},{2})", member.memberBase.nickname, " ", "") + .containsIgnoreCase(replacedNickname); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberReadSearchService.java b/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberReadSearchService.java new file mode 100644 index 00000000..ea547d8b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberReadSearchService.java @@ -0,0 +1,69 @@ +package com.haejwo.tripcometrue.domain.member.service; + +import com.haejwo.tripcometrue.domain.member.dto.response.MemberSimpleResponseDto; +import com.haejwo.tripcometrue.domain.member.exception.UserNotFoundException; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.repository.TripRecordViewHistoryRepository; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class MemberReadSearchService { + + private final MemberRepository memberRepository; + private final TripRecordViewHistoryRepository tripRecordViewHistoryRepository; + private static final int HOME_TOP_CREATORS_SIZE = 10; + + @Transactional(readOnly = true) + public MemberSimpleResponseDto getMemberSimpleInfo(Long memberId) { + return MemberSimpleResponseDto.fromEntity( + memberRepository.findById(memberId) + .orElseThrow(UserNotFoundException::new) + ); + } + + @Transactional(readOnly = true) + public List listTopMemberSimpleInfos() { + LocalDate current = LocalDate.now(); + LocalDateTime start = current.minusDays(3).atStartOfDay(); + LocalDateTime end = current.minusDays(1).atTime(23, 59, 59); + + return tripRecordViewHistoryRepository + .findTopListMembers(start, end, HOME_TOP_CREATORS_SIZE) + .stream() + .map(dto -> MemberSimpleResponseDto.builder() + .memberId(dto.memberId()) + .nickname(dto.nickname()) + .introduction(dto.introduction()) + .profileImageUrl(dto.profileImageUrl()) + .build() + ).toList(); + } + + @Transactional(readOnly = true) + public List searchByNickname(String nickname) { + return memberRepository + .findByNicknameOrderByMemberRating(nickname) + .stream() + .map(MemberSimpleResponseDto::fromEntity) + .toList(); + } + + @Transactional(readOnly = true) + public SliceResponseDto searchByNickname(String nickname, Pageable pageable) { + + return SliceResponseDto.of( + memberRepository + .findByNicknameOrderByMemberRating(nickname, pageable) + .map(MemberSimpleResponseDto::fromEntity) + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberService.java b/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberService.java index 0d813f05..e0ee0785 100644 --- a/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberService.java +++ b/src/main/java/com/haejwo/tripcometrue/domain/member/service/MemberService.java @@ -1,11 +1,20 @@ package com.haejwo.tripcometrue.domain.member.service; +import com.haejwo.tripcometrue.domain.member.dto.request.IntroductionRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.NicknameRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.PasswordRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.ProfileImageRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.request.SignUpRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.response.*; import com.haejwo.tripcometrue.domain.member.entity.Member; -import com.haejwo.tripcometrue.domain.member.exception.EmailDuplicateException; +import com.haejwo.tripcometrue.domain.member.exception.*; import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; -import com.haejwo.tripcometrue.domain.member.request.SignUpRequest; -import com.haejwo.tripcometrue.domain.member.response.SignUpResponse; -import com.haejwo.tripcometrue.global.util.ResponseDTO; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -19,16 +28,151 @@ public class MemberService { private final MemberRepository memberRepository; private final BCryptPasswordEncoder passwordEncoder; - public ResponseDTO signup(SignUpRequest signUpRequest) { + public SignUpResponseDto signup(SignUpRequestDto signUpRequestDto) { - memberRepository.findByMemberBaseEmail(signUpRequest.email()).ifPresent(user -> { + memberRepository.findByMemberBaseEmail(signUpRequestDto.email()).ifPresent(user -> { throw new EmailDuplicateException(); }); - String encodedPassword = passwordEncoder.encode(signUpRequest.password()); + String encodedPassword = passwordEncoder.encode(signUpRequestDto.password()); - Member newMember = signUpRequest.toEntity(encodedPassword); + Member newMember = signUpRequestDto.toEntity(encodedPassword, generateName()); memberRepository.save(newMember); - return ResponseDTO.okWithData(SignUpResponse.fromEntity(newMember)); + return SignUpResponseDto.fromEntity(newMember); } -} \ No newline at end of file + + public void checkDuplicateEmail(String email) { + memberRepository.findByMemberBaseEmail(email).ifPresent(user -> { + throw new EmailDuplicateException(); + }); + } + + public String generateName() { + List first = Arrays.asList("์ž์œ ๋กœ์šด", "์„œ์šดํ•œ", + "๋‹น๋‹นํ•œ", "๋ฐฐ๋ถ€๋ฅธ", "์ˆ˜์ค์€", "๋ฉ‹์žˆ๋Š”", + "์šฉ๊ธฐ์žˆ๋Š”", "์‹ฌ์‹ฌํ•œ", "์ž˜์ƒ๊ธด", "์ด์œ", "๋ˆˆ์›ƒ์Œ์น˜๋Š”", "ํ–‰๋ณตํ•œ", "์‚ฌ๋ž‘์Šค๋Ÿฌ์šด", "์ˆœ์ˆ˜ํ•œ"); + List name = Arrays.asList("์‚ฌ์ž", "์ฝ”๋ผ๋ฆฌ", "ํ˜ธ๋ž‘์ด", "๊ณฐ", "์—ฌ์šฐ", "๋Š‘๋Œ€", "๋„ˆ๊ตฌ๋ฆฌ", + "์ฐธ์ƒˆ", "๊ณ ์Šด๋„์น˜", "๊ฐ•์•„์ง€", "๊ณ ์–‘์ด", "๊ฑฐ๋ถ์ด", "ํ† ๋ผ", "์•ต๋ฌด์ƒˆ", "ํ•˜์ด์—๋‚˜", "ํŽญ๊ท„", "ํ•˜๋งˆ", + "์–ผ๋ฃฉ๋ง", "์น˜ํƒ€", "์•…์–ด", "๊ธฐ๋ฆฐ", "์ˆ˜๋‹ฌ", "์—ผ์†Œ", "๋‹ค๋žŒ์ฅ", "ํŒ๋‹ค", "์ฝ”์•Œ๋ผ", "์•ต๋ฌด์ƒˆ", "๋…์ˆ˜๋ฆฌ", "์•ŒํŒŒ์นด"); + Collections.shuffle(first); + Collections.shuffle(name); + return first.get(0) + name.get(0); + } + + public void changePassword( + PrincipalDetails principalDetails, PasswordRequestDto passwordRequestDto) { + Member member = getLoginMember(principalDetails); + + String currentPassword = member.getMemberBase().getPassword(); + String newPassword = passwordRequestDto.newPassword(); + + //์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ, ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผ์—ฌ๋ถ€ ์ตœ์ข… ๊ฒ€์ฆ + if (passwordEncoder.matches(newPassword, currentPassword)) { + throw new NewPasswordSameAsOldException(); + } + + //์ƒˆ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ๊ฐ’ ๋™์ผ์—ฌ๋ถ€ ์ตœ์ข… ๊ฒ€์ฆ + if (!passwordRequestDto.newPassword().equals(passwordRequestDto.confirmPassword())) { + throw new NewPasswordNotMatchException(); + } + + String encodedNewPassword = passwordEncoder.encode(newPassword); + member.getMemberBase().changePassword(encodedNewPassword); + memberRepository.save(member); + } + + public void checkPassword( + PrincipalDetails principalDetails, PasswordRequestDto passwordRequestDto) { + Member member = principalDetails.getMember(); + String currentPassword = member.getMemberBase().getPassword(); + String inPuttedCurrentPassword = passwordRequestDto.currentPassword(); + String newPassword = passwordRequestDto.newPassword(); + + // ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋งŒ ์ž…๋ ฅ๋œ ๊ฒฝ์šฐ + if (passwordRequestDto.currentPassword() != null + && passwordRequestDto.newPassword() == null) { + if (!passwordEncoder.matches(passwordRequestDto.currentPassword(), currentPassword)) { + throw new CurrentPasswordNotMatchException(); + } + } + + // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋งŒ ์ž…๋ ฅ๋œ ๊ฒฝ์šฐ + if (passwordRequestDto.newPassword() != null + && passwordRequestDto.currentPassword() == null) { + if (passwordEncoder.matches(passwordRequestDto.newPassword(), currentPassword)) { + throw new NewPasswordSameAsOldException(); + } + } + + // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์ด ์ž…๋ ฅ๋œ ๊ฒฝ์šฐ + if (passwordRequestDto.confirmPassword() != null) { + if (!passwordRequestDto.newPassword().equals(passwordRequestDto.confirmPassword())) { + throw new NewPasswordNotMatchException(); + } + } + } + + public ProfileImageResponseDto updateProfileImage( + PrincipalDetails principalDetails, ProfileImageRequestDto requestDto) { + Member member = getLoginMember(principalDetails); + + member.updateProfileImage(requestDto.profile_image()); + memberRepository.save(member); + + return ProfileImageResponseDto.fromEntity(member); + } + + public IntroductionResponseDto updateIntroduction( + PrincipalDetails principalDetails, IntroductionRequestDto requestDto) { + if (requestDto.introduction().length() > 20) { + throw new IntroductionLengthExceededException(); + } + + Member member = getLoginMember(principalDetails); + member.updateIntroduction(requestDto.introduction()); + + return IntroductionResponseDto.fromEntity(member); + } + + public NicknameResponseDto updateNickname( + PrincipalDetails principalDetails, NicknameRequestDto requestDto) { + Member member = memberRepository.findById(principalDetails.getMember().getId()) + .orElseThrow(); + + memberRepository.findByMemberBaseNickname(requestDto.nickname()) + .ifPresent(existingMember -> { + throw new NicknameAlreadyExistsException(); + }); + + if(member.getNickNameChangeTime() != null && + ChronoUnit.MONTHS.between(member.getNickNameChangeTime(), LocalDateTime.now()) < 6){ + throw new NicknameChangeNotAvailableException(); + } + + member.getMemberBase().changeNickname(requestDto.nickname()); + member.updateNickNameChangeCount(); + member.updateNickNameChangeTime(LocalDateTime.now()); + + return NicknameResponseDto.fromEntity(member); + } + + public void deleteAccount(PrincipalDetails principalDetails) { + + Member member = memberRepository.findById(principalDetails.getMember().getId()) + .orElseThrow(); + + memberRepository.delete(member); + } + + public Member getLoginMember(PrincipalDetails principalDetails) { + Member member = principalDetails.getMember(); + return member; + } + + public MemberDetailResponseDto getMemberDetails(PrincipalDetails principalDetails) { + Member member = getLoginMember(principalDetails); + return MemberDetailResponseDto.fromEntity(member); + } + +} + diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/controller/PlaceController.java b/src/main/java/com/haejwo/tripcometrue/domain/place/controller/PlaceController.java new file mode 100644 index 00000000..96994cab --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/controller/PlaceController.java @@ -0,0 +1,154 @@ +package com.haejwo.tripcometrue.domain.place.controller; + +import com.haejwo.tripcometrue.domain.place.dto.request.PlaceRequestDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceListItemResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceMapInfoResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceNearbyResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceResponseDto; +import com.haejwo.tripcometrue.domain.place.service.PlaceService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import java.util.List; + +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.data.web.SortDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/places") +@RequiredArgsConstructor +public class PlaceController { + + private final PlaceService placeService; + + @PostMapping + public ResponseEntity> placeAdd( + @RequestBody PlaceRequestDto requestDto + ) { + + PlaceResponseDto responseDto = placeService.addPlace(requestDto); + ResponseDTO responseBody = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/{placeId}") + public ResponseEntity> placeDetails( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeId + ) { + + PlaceResponseDto responseDto = placeService.findPlace(principalDetails, placeId); + ResponseDTO responseBody = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/{placeId}/maplist") + public ResponseEntity>> placeMapInfoList( + @PathVariable Long placeId + ) { + + List responseDtos = placeService.findPlaceMapInfoList(placeId); + + ResponseDTO> responseBody = ResponseDTO.okWithData(responseDtos); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/{placeId}/nearby") + public ResponseEntity>> placeNearbyList( + @PathVariable Long placeId + ) { + + List responseDtos = placeService.findNearbyPlaceList(placeId); + + ResponseDTO> responseBody = ResponseDTO.okWithData(responseDtos); + + return ResponseEntity.status(responseBody + .getCode()) + .body(responseBody); + } + + @GetMapping + public ResponseEntity>> placeList( + Pageable pageable, + @RequestParam Integer storedCount + ) { + + Page placePage = placeService.findPlaces(pageable, storedCount); + + ResponseDTO responseBody = ResponseDTO.okWithData(placePage); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/search") + public ResponseEntity>> searchPlacesByName( + @RequestParam("placeName") String placeName, + @PageableDefault + @SortDefault.SortDefaults({ + @SortDefault(sort = "storedCount", direction = Sort.Direction.DESC), + @SortDefault(sort = "commentCount", direction = Sort.Direction.DESC)}) + Pageable pageable + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + placeService.listPlacesByName(placeName, pageable) + ) + ); + } + + @PutMapping("/{placeId}") + public ResponseEntity> placeModify( + @PathVariable Long placeId, + @RequestBody PlaceRequestDto requestDto + ) { + + PlaceResponseDto responseDto = placeService.modifyPlace(placeId, requestDto); + ResponseDTO responseBody = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @DeleteMapping("/{placeId}") + public ResponseEntity placeRemove( + @PathVariable Long placeId + ) { + + placeService.removePlace(placeId); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/controller/PlaceControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/place/controller/PlaceControllerAdvice.java new file mode 100644 index 00000000..9f51a461 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/controller/PlaceControllerAdvice.java @@ -0,0 +1,29 @@ +package com.haejwo.tripcometrue.domain.place.controller; + +import com.haejwo.tripcometrue.domain.place.exception.PlaceNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class PlaceControllerAdvice { + + @ExceptionHandler(PlaceNotFoundException.class) + public ResponseEntity> placeNotFoundExceptionHandler( + PlaceNotFoundException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + + + } + + + + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/PlaceDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/PlaceDto.java new file mode 100644 index 00000000..237e2f49 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/PlaceDto.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.domain.place.dto; + +import java.time.LocalTime; +import lombok.Builder; + +public record PlaceDto( + Long id, + String name, + String address, + String description, + LocalTime weekdayOpenTime, + LocalTime weekdayCloseTime, + LocalTime weekendOpenTime, + LocalTime weekendCloseTime, + Integer storedCount) { + + @Builder + public PlaceDto( + Long id, + String name, + String address, + String description, + LocalTime weekdayOpenTime, + LocalTime weekdayCloseTime, + LocalTime weekendOpenTime, + LocalTime weekendCloseTime, + Integer storedCount) { + this.id = id; + this.name = name; + this.address = address; + this.description = description; + this.weekdayOpenTime = weekdayOpenTime; + this.weekdayCloseTime = weekdayCloseTime; + this.weekendOpenTime = weekendOpenTime; + this.weekendCloseTime = weekendCloseTime; + this.storedCount = storedCount; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/request/PlaceFilterRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/request/PlaceFilterRequestDto.java new file mode 100644 index 00000000..adbafccf --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/request/PlaceFilterRequestDto.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.place.dto.request; + +public record PlaceFilterRequestDto( + Integer stored_count, + Integer storedCount +) { + + // record๋Š” Compact Constructor๋ผ๋Š” ๊ธฐ๋Šฅ์žˆ์–ด, ์ƒ์„ฑ์ž ๋‚ด๋ถ€์˜ ๋ณ€์ˆ˜์— ๋Œ€ํ•œ ๋กœ์ง์ด ๋งˆ์ง€๋ง‰์œผ๋กœ ๋™์ž‘ํ•˜์—ฌ ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™”๋ฅผ ํ•œ๋‹ค. + public PlaceFilterRequestDto { + storedCount = stored_count; + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/request/PlaceRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/request/PlaceRequestDto.java new file mode 100644 index 00000000..bb444f89 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/request/PlaceRequestDto.java @@ -0,0 +1,58 @@ +package com.haejwo.tripcometrue.domain.place.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import java.time.LocalTime; +import lombok.Builder; + +public record PlaceRequestDto( + String name, + String address, + String description, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekdayOpenTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekdayCloseTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekendOpenTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekendCloseTime, + Integer storedCount, + Long cityId +) { + + @Builder + public PlaceRequestDto( + String name, + String address, + String description, + LocalTime weekdayOpenTime, + LocalTime weekdayCloseTime, + LocalTime weekendOpenTime, + LocalTime weekendCloseTime, + Integer storedCount, + Long cityId + ) { + this.name = name; + this.address = address; + this.description = description; + this.weekdayOpenTime = weekdayOpenTime; + this.weekdayCloseTime = weekdayCloseTime; + this.weekendOpenTime = weekendOpenTime; + this.weekendCloseTime = weekendCloseTime; + this.storedCount = storedCount; + this.cityId = cityId; + } + + public Place toEntity(City city) { + return Place.builder() + .name(this.name) + .address(this.address) + .description(this.description) + .weekdayOpenTime(this.weekdayOpenTime) + .weekdayCloseTime(this.weekdayCloseTime) + .weekendOpenTime(this.weekendOpenTime) + .weekendCloseTime(this.weekendCloseTime) + .storedCount(this.storedCount) + .city(city) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceListItemResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceListItemResponseDto.java new file mode 100644 index 00000000..23931849 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceListItemResponseDto.java @@ -0,0 +1,44 @@ +package com.haejwo.tripcometrue.domain.place.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import lombok.Builder; + +import java.time.LocalTime; + +public record PlaceListItemResponseDto( + Long placeId, + String placeName, + String address, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + LocalTime weekdayOpenTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + LocalTime weekdayCloseTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + LocalTime weekendOpenTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + LocalTime weekendCloseTime, + String telNumber, + String cityName, + String imageUrl +) { + + @Builder + public PlaceListItemResponseDto { + } + + public static PlaceListItemResponseDto fromEntity(Place entity, String imageUrl) { + return PlaceListItemResponseDto.builder() + .placeId(entity.getId()) + .placeName(entity.getName()) + .address(entity.getAddress()) + .weekdayOpenTime(entity.getWeekdayOpenTime()) + .weekdayCloseTime(entity.getWeekdayCloseTime()) + .weekendOpenTime(entity.getWeekendOpenTime()) + .weekendCloseTime(entity.getWeekendCloseTime()) + .telNumber(entity.getPhoneNumber()) + .cityName(entity.getCity().getName()) + .imageUrl(imageUrl) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceMapInfoResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceMapInfoResponseDto.java new file mode 100644 index 00000000..aebba7fc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceMapInfoResponseDto.java @@ -0,0 +1,46 @@ +package com.haejwo.tripcometrue.domain.place.dto.response; + +import lombok.Builder; + +public record PlaceMapInfoResponseDto( + Long placeId, + String placeName, + Double latitude, + Double longitude, + Integer storeCount, + Integer commentCount, + String imageUrl + + +) { + + @Builder + public PlaceMapInfoResponseDto(Long placeId, String placeName, Double latitude, + Double longitude, + Integer storeCount, Integer commentCount, String imageUrl) { + this.placeId = placeId; + this.placeName = placeName; + this.latitude = latitude; + this.longitude = longitude; + this.storeCount = storeCount; + this.commentCount = commentCount; + this.imageUrl = imageUrl; + } + + +// public static PlaceMapInfoResponseDto fromEntity(Place entity) { +// return PlaceMapInfoResponseDto.builder() +// .placeId(entity.getId()) +// .placeName(entity.getName()) +// .latitude(entity.getLatitude()) +// .longitude(entity.getLongitude()) +// .storeCount() +// .commentCount() +// .build(); +// } + + + + + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceNearbyResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceNearbyResponseDto.java new file mode 100644 index 00000000..3282585b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceNearbyResponseDto.java @@ -0,0 +1,28 @@ +package com.haejwo.tripcometrue.domain.place.dto.response; + +import lombok.Builder; + +public record PlaceNearbyResponseDto( + Long placeId, + String placeName, + String imageUrl, + Double latitude, + Double longitude, + Integer storedCount, + Integer reviewCount, + Integer commentCount +) { + + @Builder + public PlaceNearbyResponseDto(Long placeId, String placeName, String imageUrl, Double latitude, + Double longitude, Integer storedCount, Integer reviewCount, Integer commentCount) { + this.placeId = placeId; + this.placeName = placeName; + this.imageUrl = imageUrl; + this.latitude = latitude; + this.longitude = longitude; + this.storedCount = storedCount; + this.reviewCount = reviewCount; + this.commentCount = commentCount; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceResponseDto.java new file mode 100644 index 00000000..9e70c2a0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/dto/response/PlaceResponseDto.java @@ -0,0 +1,86 @@ +package com.haejwo.tripcometrue.domain.place.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import java.time.LocalTime; +import lombok.Builder; +public record PlaceResponseDto( + Long id, + String name, + String address, + String description, + String phoneNumber, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekdayOpenTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekdayCloseTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekendOpenTime, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") LocalTime weekendCloseTime, + Double latitude, + Double longitude, + Integer storedCount, + Boolean isStored, + Long cityId +) { + + @Builder + public PlaceResponseDto(Long id, String name, String address, String description, + String phoneNumber, + @JsonFormat(shape = Shape.STRING, pattern = "HH:mm") LocalTime weekdayOpenTime, + @JsonFormat(shape = Shape.STRING, pattern = "HH:mm") LocalTime weekdayCloseTime, + @JsonFormat(shape = Shape.STRING, pattern = "HH:mm") LocalTime weekendOpenTime, + @JsonFormat(shape = Shape.STRING, pattern = "HH:mm") LocalTime weekendCloseTime, + Double latitude, Double longitude, Integer storedCount, Boolean isStored, Long cityId) { + this.id = id; + this.name = name; + this.address = address; + this.description = description; + this.phoneNumber = phoneNumber; + this.weekdayOpenTime = weekdayOpenTime; + this.weekdayCloseTime = weekdayCloseTime; + this.weekendOpenTime = weekendOpenTime; + this.weekendCloseTime = weekendCloseTime; + this.latitude = latitude; + this.longitude = longitude; + this.storedCount = storedCount; + this.isStored = isStored; + this.cityId = cityId; + } + + public static PlaceResponseDto fromEntity(Place entity) { + return PlaceResponseDto.builder() + .id(entity.getId()) + .name(entity.getName()) + .address(entity.getAddress()) + .description(entity.getDescription()) + .phoneNumber(entity.getPhoneNumber()) + .weekdayOpenTime(entity.getWeekdayOpenTime()) + .weekdayCloseTime(entity.getWeekdayCloseTime()) + .weekendOpenTime(entity.getWeekendOpenTime()) + .weekendCloseTime(entity.getWeekendCloseTime()) + .latitude(entity.getLatitude()) + .longitude(entity.getLongitude()) + .storedCount(entity.getStoredCount()) + .cityId(entity.getCity().getId()) + .build(); + } + + public static PlaceResponseDto fromEntity(Place entity, Boolean isStored) { + return PlaceResponseDto.builder() + .id(entity.getId()) + .name(entity.getName()) + .address(entity.getAddress()) + .description(entity.getDescription()) + .phoneNumber(entity.getPhoneNumber()) + .weekdayOpenTime(entity.getWeekdayOpenTime()) + .weekdayCloseTime(entity.getWeekdayCloseTime()) + .weekendOpenTime(entity.getWeekendOpenTime()) + .weekendCloseTime(entity.getWeekendCloseTime()) + .latitude(entity.getLatitude()) + .longitude(entity.getLongitude()) + .storedCount(entity.getStoredCount()) + .isStored(isStored) + .cityId(entity.getCity().getId()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/entity/Place.java b/src/main/java/com/haejwo/tripcometrue/domain/place/entity/Place.java new file mode 100644 index 00000000..3322855c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/entity/Place.java @@ -0,0 +1,115 @@ +package com.haejwo.tripcometrue.domain.place.entity; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.place.dto.request.PlaceRequestDto; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; + +import java.time.LocalTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Place extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "place_id") + private Long id; + + @Column(nullable = false) + private String name; + @Column(nullable = false) + private String address; + private String description; + private String phoneNumber; + private LocalTime weekdayOpenTime; + private LocalTime weekdayCloseTime; + private LocalTime weekendOpenTime; + private LocalTime weekendCloseTime; + private Double latitude; + private Double longitude; + + private Integer storedCount; + private Integer reviewCount; + private Integer commentCount; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "city_id") + private City city; + + @PrePersist + public void prePersist() { + this.storedCount = this.storedCount == null ? 0 : storedCount; + this.reviewCount = 0; + this.commentCount = 0; + } + + @Builder + public Place(Long id, String name, String address, String description, String phoneNumber, + LocalTime weekdayOpenTime, LocalTime weekdayCloseTime, LocalTime weekendOpenTime, + LocalTime weekendCloseTime, Double latitude, Double longitude, Integer storedCount, + Integer reviewCount, Integer commentCount, City city) { + this.id = id; + this.name = name; + this.address = address; + this.description = description; + this.phoneNumber = phoneNumber; + this.weekdayOpenTime = weekdayOpenTime; + this.weekdayCloseTime = weekdayCloseTime; + this.weekendOpenTime = weekendOpenTime; + this.weekendCloseTime = weekendCloseTime; + this.latitude = latitude; + this.longitude = longitude; + this.storedCount = storedCount; + this.reviewCount = reviewCount; + this.commentCount = commentCount; + this.city = city; + } + + public void update(PlaceRequestDto requestDto) { + this.name = requestDto.name(); + this.address = requestDto.address(); + this.description = requestDto.description(); + this.weekdayOpenTime = requestDto.weekdayOpenTime(); + this.weekdayCloseTime = requestDto.weekdayCloseTime(); + this.weekendOpenTime = requestDto.weekendOpenTime(); + this.weekendCloseTime = requestDto.weekendCloseTime(); + this.storedCount = requestDto.storedCount(); + } + + public void incrementStoreCount() { + if(this.storedCount == null) { + this.storedCount = 1; + } else { + this.storedCount++; + } + } + + public void decrementStoreCount() { + if(this.storedCount > 0) { + this.storedCount--; + } + } + + public void incrementReviewCount() { + if(this.reviewCount == null) { + this.reviewCount = 1; + } else { + this.reviewCount++; + } + } + + public void incrementCommentCount() { + if(this.commentCount == null) { + this.commentCount = 1; + } else { + this.commentCount++; + } + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/exception/PlaceNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/place/exception/PlaceNotFoundException.java new file mode 100644 index 00000000..70ed7998 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/exception/PlaceNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.place.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class PlaceNotFoundException extends ApplicationException { + private static final ErrorCode ERROR_CODE = ErrorCode.PLACE_NOT_FOUND; + + public PlaceNotFoundException() { + super(ERROR_CODE); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepository.java new file mode 100644 index 00000000..11b25a21 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepository.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.place.repositroy; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlaceRepository extends + JpaRepository, PlaceRepositoryCustom +{ + List findByCityId(Long cityId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryCustom.java new file mode 100644 index 00000000..de1eb6d9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryCustom.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.place.repositroy; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceMapInfoResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceNearbyResponseDto; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public interface PlaceRepositoryCustom { + + Page findPlaceWithFilter(Pageable pageable, + Integer storedCount); + + Slice findPlacesByCityIdAndPlaceName(Long cityId, String placeName, Pageable pageable); + + Slice findPlacesWithCityByPlaceName(String placeName, Pageable pageable); + + List findPlacesByCityAndOrderByStoredCountLimitSize(City city, int size); + + List findPlaceMapInfoListByPlaceId(Long placeId); + + List findNearbyPlaces(Long placeId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryImpl.java new file mode 100644 index 00000000..0872294f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryImpl.java @@ -0,0 +1,281 @@ +package com.haejwo.tripcometrue.domain.place.repositroy; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.entity.QCity; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceMapInfoResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceNearbyResponseDto; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.entity.QPlace; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordScheduleImage; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.Tuple; +import com.querydsl.core.types.NullExpression; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.OrderSpecifier.NullHandling; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import org.springframework.util.StringUtils; + +import static com.haejwo.tripcometrue.domain.city.entity.QCity.city; +import static com.haejwo.tripcometrue.domain.place.entity.QPlace.place; + +public class PlaceRepositoryImpl extends QuerydslRepositorySupport implements PlaceRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public PlaceRepositoryImpl(JPAQueryFactory queryFactory) { + super(Place.class); + this.queryFactory = queryFactory; + } + + @Override + public Page findPlaceWithFilter(Pageable pageable, Integer storedCount) { + + QPlace place = QPlace.place; + BooleanBuilder booleanBuilder = new BooleanBuilder(); + + if (storedCount != null && storedCount >= 0) { + booleanBuilder.and(place.storedCount.goe(storedCount)); + } + + List result = from(place) + .where(booleanBuilder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // ํ”„๋ก ํŠธ์˜ Page ์ •๋ณด ํ•„์š” ์œ ๋ฌด์— ๋”ฐ๋ผ ์‘๋‹ต ๊ฐ์ฒด List, Page ๋‚˜๋‰จ + long total = from(place).where(booleanBuilder).fetchCount(); + + return new PageImpl<>(result, pageable, total); + + } + + @Override + public Slice findPlacesByCityIdAndPlaceName(Long cityId, String placeName, Pageable pageable) { + + int pageSize = pageable.getPageSize(); + List content = queryFactory + .selectFrom(place) + .join(place.city, city) + .where( + city.id.eq(cityId), + containsIgnoreCasePlaceName(placeName) + ) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .orderBy(getSort(pageable)) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice findPlacesWithCityByPlaceName(String placeName, Pageable pageable) { + + int pageSize = pageable.getPageSize(); + List content = queryFactory + .selectFrom(place) + .join(place.city, city).fetchJoin() + .where( + containsIgnoreCasePlaceName(placeName) + ) + .orderBy(getSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public List findPlacesByCityAndOrderByStoredCountLimitSize(City city, int size) { + + return queryFactory + .selectFrom(place) + .join(place.city) + .where(place.city.eq(city)) + .orderBy(place.storedCount.desc(), place.createdAt.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findPlaceMapInfoListByPlaceId(Long placeId) { + + QPlace qPlace = place; + QCity qCity = city; + QTripRecordSchedule qTripRecordSchedule = QTripRecordSchedule.tripRecordSchedule; + QTripRecordScheduleImage qTripRecordScheduleImage = QTripRecordScheduleImage.tripRecordScheduleImage; + + List places = queryFactory + .selectFrom(qPlace) + .join(qPlace.city, qCity) + .where(qCity.id.eq( + queryFactory + .select(qCity.id) + .from(qPlace) + .where(qPlace.id.eq(placeId)) + .fetchOne() + )) + .fetch(); + + List placeIds = places.stream().map(Place::getId).collect(Collectors.toList()); + + List images = queryFactory + .select(qTripRecordSchedule.place.id, qTripRecordScheduleImage.imageUrl.min()) + .from(qTripRecordSchedule) + .join(qTripRecordSchedule.tripRecordScheduleImages, qTripRecordScheduleImage) + .where(qTripRecordSchedule.place.id.in(placeIds)) + .groupBy(qTripRecordSchedule.place.id) + .fetch(); + + List result = placeIds.stream() + .map(id -> { // placeId๋ฅผ id๋กœ ๋ณ€๊ฒฝ + String imageUrl = images.stream() + .filter(tuple -> tuple.get(0, Long.class).equals(id)) + .map(tuple -> tuple.get(1, String.class)) + .findFirst() + .orElse(null); + Place place = places.stream() + .filter(p -> p.getId().equals(id)) + .findFirst() + .orElseThrow(); + return PlaceMapInfoResponseDto.builder() + .placeId(place.getId()) + .placeName(place.getName()) + .latitude(place.getLatitude()) + .longitude(place.getLongitude()) + .storeCount(place.getStoredCount()) + .commentCount(place.getCommentCount()) + .imageUrl(imageUrl) + .build(); + }) + .collect(Collectors.toList()); + + return result; + + } + + @Override + public List findNearbyPlaces(Long placeId) { + + QPlace qPlace = QPlace.place; + QTripRecordSchedule qTripRecordSchedule = QTripRecordSchedule.tripRecordSchedule; + QTripRecordScheduleImage qTripRecordScheduleImage = QTripRecordScheduleImage.tripRecordScheduleImage; + + // ์ฃผ์–ด์ง„ placeId์— ํ•ด๋‹นํ•˜๋Š” ์žฅ์†Œ์˜ ์œ„๋„, ๊ฒฝ๋„๋ฅผ ์ฐพ๋Š”๋‹ค. + Place centerPlace = queryFactory + .selectFrom(qPlace) + .where(qPlace.id.eq(placeId)) + .fetchOne(); + + // ์ฃผ๋ณ€ ์žฅ์†Œ๋ฅผ ๋ฝ‘์•„๋ƒ…๋‹ˆ๋‹ค. (1๋„ ๋ฒ”์œ„ ๋‚ด๋ฉด ๋ณดํ†ต 11km ์•ˆ์ชฝ์ž„) + List nearbyPlaces = queryFactory + .selectFrom(qPlace) + .where(qPlace.latitude.between(centerPlace.getLatitude() - 1, centerPlace.getLatitude() + 1) + .and(qPlace.longitude.between(centerPlace.getLongitude() - 1, centerPlace.getLongitude() + 1))) + .orderBy(qPlace.storedCount.desc()) + .where(qPlace.id.ne(placeId)) + .limit(5) + .fetch(); + + // ๊ฐ ์žฅ์†Œ์— ํ•ด๋‹นํ•˜๋Š” ์Šค์ผ€์ค„ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์•„ PlaceNearbyResponseDto ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. (์–ด์ฐจํ”ผ ์ปจํ…์ธ ๊ฐ€ 5๊ฐœ ๊ณ ์ •์ด์—ฌ์„œ ์ด๊ฒŒ ๋” ๊ฐ„๋‹จํ•จ) + List result = nearbyPlaces.stream() + .map(place -> { + String imageUrl = queryFactory + .select(qTripRecordScheduleImage.imageUrl) + .from(qTripRecordScheduleImage) + .join(qTripRecordScheduleImage.tripRecordSchedule, qTripRecordSchedule) + .where(qTripRecordSchedule.place.id.eq(place.getId())) + .orderBy(qTripRecordScheduleImage.id.asc()) + .fetchFirst(); + + return new PlaceNearbyResponseDto( + place.getId(), + place.getName(), + imageUrl, + place.getLatitude(), + place.getLongitude(), + place.getStoredCount(), + place.getReviewCount(), + place.getCommentCount() + ); + }) + .collect(Collectors.toList()); + + return result; + } + + private BooleanExpression containsIgnoreCasePlaceName(String placeName) { + if (!StringUtils.hasText(placeName)) { + return null; + } + + String replacedWhitespace = placeName.replaceAll(" ", ""); + + return Expressions.stringTemplate( + "function('replace',{0},{1},{2})", place.name, " ", "" + ).containsIgnoreCase(replacedWhitespace); + } + + private OrderSpecifier[] getSort(Pageable pageable) { + + List> orderSpecifiers = new LinkedList<>(); + if (!pageable.getSort().isEmpty()) { + for (Sort.Order sortOrder : pageable.getSort()) { + Order direction = sortOrder.getDirection().isAscending() ? Order.ASC : Order.DESC; + + String property = sortOrder.getProperty(); + switch (property) { + case "id": + orderSpecifiers.add(new OrderSpecifier<>(direction, place.id)); + break; + case "createdAt": + orderSpecifiers.add(new OrderSpecifier<>(direction, place.createdAt)); + break; + case "storedCount": + orderSpecifiers.add(new OrderSpecifier<>(direction, place.storedCount)); + break; + case "commentCount": + orderSpecifiers.add(new OrderSpecifier<>(direction, place.commentCount)); + break; + } + } + } + + // ์•„๋ฌด ์กฐ๊ฑด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, order by null + if(orderSpecifiers.isEmpty()) { + orderSpecifiers.add(new OrderSpecifier(Order.ASC, NullExpression.DEFAULT, NullHandling.Default)); + } + + return orderSpecifiers.toArray(OrderSpecifier[]::new); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/place/service/PlaceService.java b/src/main/java/com/haejwo/tripcometrue/domain/place/service/PlaceService.java new file mode 100644 index 00000000..f4723919 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/place/service/PlaceService.java @@ -0,0 +1,147 @@ +package com.haejwo.tripcometrue.domain.place.service; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.place.dto.request.PlaceRequestDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceListItemResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceMapInfoResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceNearbyResponseDto; +import com.haejwo.tripcometrue.domain.place.dto.response.PlaceResponseDto; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.exception.PlaceNotFoundException; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.store.repository.PlaceStoreRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import java.util.List; +import java.util.Objects; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleImageWithPlaceIdQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image.TripRecordScheduleImageRepository; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PlaceService { + + private final PlaceRepository placeRepository; + private final CityRepository cityRepository; + private final TripRecordScheduleImageRepository tripRecordScheduleImageRepository; + private final PlaceStoreRepository placeStoreRepository; + + @Transactional + public PlaceResponseDto addPlace(PlaceRequestDto requestDto) { + City city = cityRepository.findById(requestDto.cityId()) + .orElseThrow(CityNotFoundException::new); + Place requestPlace = requestDto.toEntity(city); + Place savedPlace = placeRepository.save(requestPlace); + PlaceResponseDto responseDto = PlaceResponseDto.fromEntity(savedPlace); + + return responseDto; + + } + + @Transactional(readOnly = true) + public PlaceResponseDto findPlace(PrincipalDetails principalDetails, Long placeId) { + + Place findPlace = findPlaceById(placeId); + + Boolean isStored = false; + if(principalDetails != null) { + isStored = placeStoreRepository.existsByMemberAndPlace(principalDetails.getMember(), findPlace); + } + + PlaceResponseDto responseDto = PlaceResponseDto.fromEntity(findPlace, isStored); + + return responseDto; + } + + @Transactional(readOnly = true) + public Page findPlaces(Pageable pageable, Integer storedCount) { + + Page findPlaces = placeRepository.findPlaceWithFilter(pageable, storedCount); + + Page result = findPlaces.map(PlaceResponseDto::fromEntity); + + return result; + + } + + @Transactional(readOnly = true) + public SliceResponseDto listPlacesByName( + String placeName, Pageable pageable + ) { + Slice places = placeRepository.findPlacesWithCityByPlaceName(placeName, pageable); + + return SliceResponseDto.of( + places.map( + place -> + PlaceListItemResponseDto + .fromEntity( + place, + // ์—ฌํ–‰์ง€ ๋Œ€ํ‘œ์ด๋ฏธ์ง€ ์ถ”์ถœ + tripRecordScheduleImageRepository + .findInPlaceIdsOrderByCreatedAtDesc( + places.getContent() + .stream() + .map(Place::getId) + .toList() + ) + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(TripRecordScheduleImageWithPlaceIdQueryDto::imageUrl) + .orElse(null) + ) + ) + ); + } + + public List findPlaceMapInfoList(Long placeId) { + + List responseDtos = placeRepository.findPlaceMapInfoListByPlaceId(placeId); + + return responseDtos; + } + + public List findNearbyPlaceList(Long placeId) { + + List responseDtos = placeRepository.findNearbyPlaces(placeId); + + return responseDtos; + + } + + @Transactional + public PlaceResponseDto modifyPlace(Long placeId, PlaceRequestDto requestDto) { + + Place place = findPlaceById(placeId); + place.update(requestDto); + PlaceResponseDto responseDto = PlaceResponseDto.fromEntity(place); + + return responseDto; + } + + @Transactional + public void removePlace(Long placeId) { + Place findPlace = findPlaceById(placeId); + placeRepository.delete(findPlace); + } + + private Place findPlaceById(Long placeId) { + + Place findPlace = placeRepository.findById(placeId) + .orElseThrow(PlaceNotFoundException::new); + + return findPlace; + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/global/PointType.java b/src/main/java/com/haejwo/tripcometrue/domain/review/global/PointType.java new file mode 100644 index 00000000..574f4daf --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/global/PointType.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.review.global; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PointType { + + ONLY_ONE_POINT(1), + TWO_POINTS(2); + + private final int point; +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/controller/PlaceReviewController.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/controller/PlaceReviewController.java new file mode 100644 index 00000000..6cabddfa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/controller/PlaceReviewController.java @@ -0,0 +1,100 @@ +package com.haejwo.tripcometrue.domain.review.placereview.controller; + +import com.haejwo.tripcometrue.domain.review.placereview.dto.request.DeletePlaceReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.request.PlaceReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.PlaceReviewListResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.PlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.RegisterPlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete.DeletePlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.service.PlaceReviewService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.CREATED; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/places") +public class PlaceReviewController { + + private final PlaceReviewService placeReviewService; + + @PostMapping("/{placeId}/reviews") + public ResponseEntity> registerPlaceReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeId, + @RequestBody @Validated PlaceReviewRequestDto requestDto + ) { + + RegisterPlaceReviewResponseDto responseDto = placeReviewService + .savePlaceReview(principalDetails, placeId, requestDto); + return ResponseEntity + .status(CREATED) + .body(ResponseDTO.successWithData(CREATED, responseDto)); + } + + @GetMapping("/reviews/{placeReviewId}") + public ResponseEntity> getOnePlaceReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewId + ) { + + PlaceReviewResponseDto responseDto = placeReviewService + .getPlaceReview(principalDetails, placeReviewId); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @PutMapping("/reviews/{placeReviewId}") + public ResponseEntity> modifyPlaceReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeReviewId, + @RequestBody @Validated PlaceReviewRequestDto requestDto + ) { + + PlaceReviewResponseDto responseDto = placeReviewService + .modifyPlaceReview(principalDetails, placeReviewId, requestDto); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @DeleteMapping("/reviews") + public ResponseEntity> removePlaceReviews( + @RequestBody DeletePlaceReviewRequestDto requestDto + ) { + + DeletePlaceReviewResponseDto responseDto = + placeReviewService.deletePlaceReviews(requestDto); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @GetMapping("/{placeId}/reviews") + public ResponseEntity> getPlaceReviewList( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long placeId, + @RequestParam(defaultValue = "false") boolean onlyImage, + Pageable pageable + ) { + + PlaceReviewListResponseDto responseDtos = + placeReviewService.getPlaceReviewList(principalDetails, placeId, onlyImage, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDtos)); + } + + @GetMapping("/reviews/my") + public ResponseEntity> getMyPlaceReviews( + @AuthenticationPrincipal PrincipalDetails principalDetails, + Pageable pageable + ) { + + PlaceReviewListResponseDto responseDtos + = placeReviewService.getMyPlaceReviewList(principalDetails, pageable); + return ResponseEntity.ok((ResponseDTO.okWithData(responseDtos))); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/controller/PlaceReviewControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/controller/PlaceReviewControllerAdvice.java new file mode 100644 index 00000000..99c36870 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/controller/PlaceReviewControllerAdvice.java @@ -0,0 +1,41 @@ +package com.haejwo.tripcometrue.domain.review.placereview.controller; + +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewAlreadyExistsException; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewDeleteAllFailureException; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class PlaceReviewControllerAdvice { + + @ExceptionHandler(PlaceReviewNotFoundException.class) + public ResponseEntity> handlePlaceReviewNotFoundException(PlaceReviewNotFoundException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(PlaceReviewAlreadyExistsException.class) + public ResponseEntity> handlePlaceReviewAlreadyExistsException(PlaceReviewAlreadyExistsException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(PlaceReviewDeleteAllFailureException.class) + public ResponseEntity> handlePlaceReviewDeleteAllFailureException(PlaceReviewDeleteAllFailureException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/request/DeletePlaceReviewRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/request/DeletePlaceReviewRequestDto.java new file mode 100644 index 00000000..fee695d2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/request/DeletePlaceReviewRequestDto.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.request; + +import java.util.List; + +public record DeletePlaceReviewRequestDto( + + List placeReviewIds + +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/request/PlaceReviewRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/request/PlaceReviewRequestDto.java new file mode 100644 index 00000000..a4580564 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/request/PlaceReviewRequestDto.java @@ -0,0 +1,26 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.request; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.Length; + +public record PlaceReviewRequestDto( + + String imageUrl, + + @NotBlank + @Length(min = 10, max = 2000, message = "์ž‘์„ฑ ํ—ˆ์šฉ ๋ฒ”์œ„๋Š” ์ตœ์†Œ 10์ž ๋˜๋Š” ์ตœ๋Œ€ 2,000์ž ์ž…๋‹ˆ๋‹ค.") + String content + +) { + public static PlaceReview toEntity(Member member, Place place, PlaceReviewRequestDto requestDto) { + return PlaceReview.builder() + .member(member) + .place(place) + .imageUrl(requestDto.imageUrl) + .content(requestDto.content) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/PlaceReviewListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/PlaceReviewListResponseDto.java new file mode 100644 index 00000000..df42fdaa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/PlaceReviewListResponseDto.java @@ -0,0 +1,29 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.response; + +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record PlaceReviewListResponseDto( + + Long totalCount, + int nowPageNumber, + boolean isFirst, + boolean isLast, + List placeReviews + +) { + public static PlaceReviewListResponseDto fromResponseDtos( + Page reviews, + List placeReviews + ) { + return new PlaceReviewListResponseDto( + reviews.getTotalElements(), + reviews.getNumber(), + reviews.isFirst(), + reviews.isLast(), + placeReviews + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/PlaceReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/PlaceReviewResponseDto.java new file mode 100644 index 00000000..8150377c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/PlaceReviewResponseDto.java @@ -0,0 +1,81 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.comment.placereview.dto.response.PlaceReviewCommentResponseDto; +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import org.springframework.data.domain.Slice; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +public record PlaceReviewResponseDto( + + Long placeReviewId, + Long memberId, + String nickname, + String profileUrl, + String imageUrl, + String content, + Integer likeCount, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt, + + boolean amILike, + Integer commentCount, + List comments + +) { + + public static PlaceReviewResponseDto fromEntityWithComment( + PlaceReview placeReview, + boolean amILike, + Slice placeReviewComments, + Member member + ) { + + return new PlaceReviewResponseDto( + placeReview.getId(), + placeReview.getMember().getId(), + placeReview.getMember().getMemberBase().getNickname(), + placeReview.getMember().getProfileImage(), + placeReview.getImageUrl(), + placeReview.getContent(), + placeReview.getLikeCount(), + placeReview.getCreatedAt(), + amILike, + placeReview.getCommentCount(), + placeReviewComments.map(placeReviewComment -> { + if (placeReviewComment.getParentComment() == null) { + return PlaceReviewCommentResponseDto.fromEntity(placeReviewComment, member); + } + return null; + }) + .filter(Objects::nonNull) + .toList() + ); + } + + public static PlaceReviewResponseDto fromEntity( + PlaceReview placeReview, + boolean amILike + ) { + + return new PlaceReviewResponseDto( + placeReview.getId(), + placeReview.getMember().getId(), + placeReview.getMember().getMemberBase().getNickname(), + placeReview.getMember().getProfileImage(), + placeReview.getImageUrl(), + placeReview.getContent(), + placeReview.getLikeCount(), + placeReview.getCreatedAt(), + amILike, + placeReview.getCommentCount(), + null + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/RegisterPlaceReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/RegisterPlaceReviewResponseDto.java new file mode 100644 index 00000000..cf457c3e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/RegisterPlaceReviewResponseDto.java @@ -0,0 +1,32 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; + +import java.time.LocalDateTime; + +public record RegisterPlaceReviewResponseDto( + + Long placeReviewId, + Long memberId, + String nickname, + String profileUrl, + String imageUrl, + String content, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt + +) { + public static RegisterPlaceReviewResponseDto fromEntity(PlaceReview placeReview) { + return new RegisterPlaceReviewResponseDto( + placeReview.getId(), + placeReview.getMember().getId(), + placeReview.getMember().getMemberBase().getNickname(), + placeReview.getMember().getProfileImage(), + placeReview.getImageUrl(), + placeReview.getContent(), + placeReview.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeleteAllSuccessPlaceReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeleteAllSuccessPlaceReviewResponseDto.java new file mode 100644 index 00000000..3ac225c3 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeleteAllSuccessPlaceReviewResponseDto.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete; + +import lombok.Getter; + +@Getter +public class DeleteAllSuccessPlaceReviewResponseDto implements DeletePlaceReviewResponseDto { + + private final String successMessage = "์„ฑ๊ณต์ ์œผ๋กœ ๋ชจ๋“  ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ(๋“ค)์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค."; +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeletePlaceReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeletePlaceReviewResponseDto.java new file mode 100644 index 00000000..cb66231b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeletePlaceReviewResponseDto.java @@ -0,0 +1,4 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete; + +public interface DeletePlaceReviewResponseDto { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeleteSomeFailurePlaceReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeleteSomeFailurePlaceReviewResponseDto.java new file mode 100644 index 00000000..97d0b8aa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/dto/response/delete/DeleteSomeFailurePlaceReviewResponseDto.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class DeleteSomeFailurePlaceReviewResponseDto implements DeletePlaceReviewResponseDto { + + private final String errorMessage = "์‚ญ์ œ์— ์‹คํŒจํ•œ ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ(๋“ค)์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."; + private final List failedPlaceReviewIds; + + public DeleteSomeFailurePlaceReviewResponseDto(List failedIds) { + this.failedPlaceReviewIds = failedIds; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/entity/PlaceReview.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/entity/PlaceReview.java new file mode 100644 index 00000000..b710fae5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/entity/PlaceReview.java @@ -0,0 +1,100 @@ +package com.haejwo.tripcometrue.domain.review.placereview.entity; + +import com.haejwo.tripcometrue.domain.likes.entity.PlaceReviewLikes; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.review.placereview.dto.request.PlaceReviewRequestDto; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static com.haejwo.tripcometrue.domain.review.global.PointType.ONLY_ONE_POINT; +import static com.haejwo.tripcometrue.domain.review.global.PointType.TWO_POINTS; +import static jakarta.persistence.CascadeType.REMOVE; +import static jakarta.persistence.FetchType.LAZY; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PlaceReview extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "place_review_id") + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "place_id") + private Place place; + + @OneToMany(mappedBy = "placeReview", cascade = REMOVE, orphanRemoval = true) + private List placeReviewLikeses = new ArrayList<>(); + + @Column(nullable = false) + private String content; + + private String imageUrl; + private Integer likeCount; + private Integer commentCount; + private boolean hasAnyRegisteredPhotoUrl; + + @Builder + public PlaceReview(Member member, Place place, String content, String imageUrl) { + this.member = member; + this.place = place; + this.content = content; + this.imageUrl = imageUrl; + } + + public void save(PlaceReviewRequestDto requestDto, Member member) { + if (requestDto.imageUrl() != null) { + member.earnPoint(TWO_POINTS.getPoint()); + hasAnyRegisteredPhotoUrl = true; + return; + } + + member.earnPoint(ONLY_ONE_POINT.getPoint()); + } + + public void update(PlaceReviewRequestDto requestDto, Member member) { + this.content = requestDto.content(); + + if (!hasAnyRegisteredPhotoUrl && requestDto.imageUrl() != null) { + member.earnPoint(ONLY_ONE_POINT.getPoint()); + hasAnyRegisteredPhotoUrl = true; + } + this.imageUrl = requestDto.imageUrl(); + } + + public void increaseLikesCount() { + likeCount += 1; + } + + public void decreaseLikesCount() { + likeCount -= 1; + } + + public void increaseCommentCount() { + commentCount += 1; + } + + public void decreaseCommentCount(int count) { + this.commentCount -= count; + } + + @PrePersist + private void init() { + likeCount = 0; + commentCount = 0; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewAlreadyExistsException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewAlreadyExistsException.java new file mode 100644 index 00000000..ac20b391 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewAlreadyExistsException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.placereview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class PlaceReviewAlreadyExistsException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.PLACE_REVIEW_ALREADY_EXISTS; + + public PlaceReviewAlreadyExistsException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewDeleteAllFailureException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewDeleteAllFailureException.java new file mode 100644 index 00000000..b6792b05 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewDeleteAllFailureException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.placereview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class PlaceReviewDeleteAllFailureException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.PLACE_REVIEW_DELETE_ALL_FAILURE; + + public PlaceReviewDeleteAllFailureException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewNotFoundException.java new file mode 100644 index 00000000..886b700b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/exception/PlaceReviewNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.placereview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class PlaceReviewNotFoundException extends ApplicationException { + + private static ErrorCode ERROR_CODE = ErrorCode.PLACE_REVIEW_NOT_FOUND; + + public PlaceReviewNotFoundException(){ + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepository.java new file mode 100644 index 00000000..739d8776 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepository.java @@ -0,0 +1,20 @@ +package com.haejwo.tripcometrue.domain.review.placereview.repository; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PlaceReviewRepository extends JpaRepository, PlaceReviewRepositoryCustom { + + boolean existsByMemberAndPlace(Member member, Place place); + + @Query("select pr from PlaceReview pr join fetch pr.member m where pr.member = :member order by pr.createdAt desc") + Page findByMember(@Param("member") Member member, Pageable pageable); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepositoryCustom.java new file mode 100644 index 00000000..738b0fa4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.review.placereview.repository; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface PlaceReviewRepositoryCustom { + + Page findByPlace(Place place, boolean onlyImage, Pageable pageable); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepositoryCustomImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepositoryCustomImpl.java new file mode 100644 index 00000000..fd3e0675 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/repository/PlaceReviewRepositoryCustomImpl.java @@ -0,0 +1,67 @@ +package com.haejwo.tripcometrue.domain.review.placereview.repository; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.List; + +import static com.haejwo.tripcometrue.domain.review.placereview.entity.QPlaceReview.placeReview; + +public class PlaceReviewRepositoryCustomImpl implements PlaceReviewRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public PlaceReviewRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findByPlace(Place place, boolean onlyImage, Pageable pageable) { + + List content = queryFactory + .selectFrom(placeReview) + .join(placeReview.place).on(placeReview.place.id.eq(place.getId())) + .where(imageIsNotNull(onlyImage)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(getOrderSpecifier(pageable.getSort())) + .fetch(); + + JPAQuery count = queryFactory + .select(placeReview.count()) + .from(placeReview) + .where(placeReview.place.id.eq(place.getId()), imageIsNotNull(onlyImage)); + + return PageableExecutionUtils.getPage(content, pageable, count::fetchOne); + } + + private BooleanExpression imageIsNotNull(boolean onlyImage) { + if (onlyImage) { + return placeReview.imageUrl.isNotNull(); + } + return null; + } + + private OrderSpecifier[] getOrderSpecifier(Sort sort) { + return sort.stream() + .map(order -> { + PathBuilder entityPath = new PathBuilder<>(PlaceReview.class, "placeReview"); + String property = order.getProperty(); + OrderSpecifier orderSpecifier = order.isAscending() ? + entityPath.getString(property).asc() : + entityPath.getString(property).desc(); + return orderSpecifier; + }) + .toArray(OrderSpecifier[]::new); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/service/PlaceReviewService.java b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/service/PlaceReviewService.java new file mode 100644 index 00000000..14988644 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/placereview/service/PlaceReviewService.java @@ -0,0 +1,217 @@ +package com.haejwo.tripcometrue.domain.review.placereview.service; + +import com.haejwo.tripcometrue.domain.comment.placereview.entity.PlaceReviewComment; +import com.haejwo.tripcometrue.domain.comment.placereview.repository.PlaceReviewCommentRepository; +import com.haejwo.tripcometrue.domain.likes.entity.PlaceReviewLikes; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.exception.UserInvalidAccessException; +import com.haejwo.tripcometrue.domain.member.exception.UserNotFoundException; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.exception.PlaceNotFoundException; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.review.placereview.dto.request.DeletePlaceReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.request.PlaceReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.PlaceReviewListResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.PlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.RegisterPlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete.DeleteAllSuccessPlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete.DeletePlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.dto.response.delete.DeleteSomeFailurePlaceReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.placereview.entity.PlaceReview; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewAlreadyExistsException; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewDeleteAllFailureException; +import com.haejwo.tripcometrue.domain.review.placereview.exception.PlaceReviewNotFoundException; +import com.haejwo.tripcometrue.domain.review.placereview.repository.PlaceReviewRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PlaceReviewService { + + private final PlaceReviewRepository placeReviewRepository; + private final PlaceRepository placeRepository; + private final MemberRepository memberRepository; + private final PlaceReviewCommentRepository placeReviewCommentRepository; + + @Transactional + public RegisterPlaceReviewResponseDto savePlaceReview( + PrincipalDetails principalDetails, + Long placeId, + PlaceReviewRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + Place place = getPlaceById(placeId); + + isAlreadyPlaceReviewExists(loginMember, place); + + PlaceReview placeReview = PlaceReviewRequestDto.toEntity(loginMember, place, requestDto); + placeReview.save(requestDto, loginMember); + + return RegisterPlaceReviewResponseDto.fromEntity(placeReviewRepository.save(placeReview)); + } + + private Member getMember(PrincipalDetails principalDetails) { + if (principalDetails == null) { + return null; + } + return memberRepository.findById(principalDetails.getMember().getId()) + .orElseThrow(UserNotFoundException::new); + } + + private Place getPlaceById(Long placeId) { + return placeRepository.findById(placeId) + .orElseThrow(PlaceNotFoundException::new); + } + + private void isAlreadyPlaceReviewExists(Member member, Place place) { + if (placeReviewRepository.existsByMemberAndPlace(member, place)) { + throw new PlaceReviewAlreadyExistsException(); + } + } + + public PlaceReviewResponseDto getPlaceReview(PrincipalDetails principalDetails, Long placeReviewId) { + + PlaceReview placeReview = getPlaceReviewById(placeReviewId); + boolean hasLiked = false; + + if (isLoggedIn(principalDetails)) { + hasLiked = hasLikedPlaceReview(principalDetails, placeReview); + } + + Member loginMember = getMember(principalDetails); + Slice comments = getPlaceReviewSlice(placeReview); + + return PlaceReviewResponseDto.fromEntityWithComment(placeReview, hasLiked, comments, loginMember); + } + + private PlaceReview getPlaceReviewById(Long placeReviewId) { + return placeReviewRepository.findById(placeReviewId) + .orElseThrow(PlaceReviewNotFoundException::new); + } + + private boolean isLoggedIn(PrincipalDetails principalDetails) { + return principalDetails != null; + } + + private boolean hasLikedPlaceReview(PrincipalDetails principalDetails, PlaceReview placeReview) { + List memberIds = placeReview.getPlaceReviewLikeses().stream() + .map(PlaceReviewLikes::getMember) + .map(Member::getId) + .toList(); + return memberIds.contains(principalDetails.getMember().getId()); + } + + private Slice getPlaceReviewSlice(PlaceReview placeReview) { + return placeReviewCommentRepository.findByPlaceReviewOrderByCreatedAtDesc(placeReview, null); + } + + @Transactional + public PlaceReviewResponseDto modifyPlaceReview( + PrincipalDetails principalDetails, + Long placeReviewId, + PlaceReviewRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + PlaceReview placeReview = getPlaceReviewById(placeReviewId); + + validateRightMemberAccess(loginMember, placeReview); + placeReview.update(requestDto, loginMember); + Slice comments = getPlaceReviewSlice(placeReview); + + return PlaceReviewResponseDto + .fromEntityWithComment(placeReview, hasLikedPlaceReview(principalDetails, placeReview), comments, loginMember); + } + + private void validateRightMemberAccess(Member member, PlaceReview placeReview) { + if (!Objects.equals(placeReview.getMember().getId(), member.getId())) { + throw new UserInvalidAccessException(); + } + } + + @Transactional + public DeletePlaceReviewResponseDto deletePlaceReviews( + DeletePlaceReviewRequestDto requestDto + ) { + + List placeReviewIds = requestDto.placeReviewIds(); + List failedIds = new ArrayList<>(); + + placeReviewIds.forEach(placeReviewId -> { + if (placeReviewRepository.existsById(placeReviewId)) { + placeReviewRepository.deleteById(placeReviewId); + } else { + failedIds.add(placeReviewId); + } + }); + + if (isDeleteAllFail(placeReviewIds, failedIds)) { + throw new PlaceReviewDeleteAllFailureException(); + } + if (!failedIds.isEmpty()) { + return new DeleteSomeFailurePlaceReviewResponseDto(failedIds); + } + + return new DeleteAllSuccessPlaceReviewResponseDto(); + } + + private boolean isDeleteAllFail(List placeReviewIds, List failedIds) { + return placeReviewIds.size() == failedIds.size(); + } + + public PlaceReviewListResponseDto getPlaceReviewList( + PrincipalDetails principalDetails, + Long placeId, + boolean onlyImage, + Pageable pageable + ) { + + Place place = getPlaceById(placeId); + Page reviews = placeReviewRepository.findByPlace(place, onlyImage, pageable); + + return PlaceReviewListResponseDto.fromResponseDtos( + reviews, + reviews.map(placeReview -> { + boolean hasLiked = false; + if (principalDetails != null) { + hasLiked = hasLikedPlaceReview(principalDetails, placeReview); + } + return PlaceReviewResponseDto.fromEntity( + placeReview, + hasLiked + ); + } + ).toList()); + } + + public PlaceReviewListResponseDto getMyPlaceReviewList( + PrincipalDetails principalDetails, + Pageable pageable + ) { + + Member loginMember = getMember(principalDetails); + Page reviews = placeReviewRepository.findByMember(loginMember, pageable); + + return PlaceReviewListResponseDto.fromResponseDtos( + reviews, + reviews.map(placeReview -> PlaceReviewResponseDto.fromEntity( + placeReview, + hasLikedPlaceReview(principalDetails, placeReview)) + ).toList()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/controller/TripRecordReviewController.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/controller/TripRecordReviewController.java new file mode 100644 index 00000000..4a4669d2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/controller/TripRecordReviewController.java @@ -0,0 +1,121 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.controller; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.DeleteTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.EvaluateTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.ModifyTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.RegisterTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.EvaluateTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.SimpleTripRecordResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.TripRecordReviewListResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete.DeleteTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest.LatestReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.service.TripRecordReviewService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.CREATED; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/trip-records") +public class TripRecordReviewController { + + private final TripRecordReviewService tripRecordReviewService; + + @PostMapping("/{tripRecordId}/reviews") + public ResponseEntity> evaluateTripRecord( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId, + @RequestBody @Valid EvaluateTripRecordReviewRequestDto requestDto + ) { + + EvaluateTripRecordReviewResponseDto responseDto = + tripRecordReviewService.saveRatingScore(principalDetails, tripRecordId, requestDto); + return ResponseEntity + .status(CREATED) + .body(ResponseDTO.successWithData(CREATED, responseDto)); + } + + @PutMapping("/reviews/{tripRecordReviewId}") + public ResponseEntity> modifyReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordReviewId, + @RequestBody @Valid ModifyTripRecordReviewRequestDto requestDto + ) { + + tripRecordReviewService.modifyTripRecordReview(principalDetails, tripRecordReviewId, requestDto); + return ResponseEntity.ok().body(ResponseDTO.ok()); + } + + @PutMapping("/reviews/{tripRecordReviewId}/contents") + public ResponseEntity> registerReviewContent( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordReviewId, + @RequestBody @Valid RegisterTripRecordReviewRequestDto requestDto + ) { + + tripRecordReviewService.registerContent(principalDetails, tripRecordReviewId, requestDto); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @GetMapping("/{tripRecordId}/reviews/latest") + public ResponseEntity> getLatestReviewAndMyScore( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId + ) { + + LatestReviewResponseDto responseDto = + tripRecordReviewService.getLatestReview(principalDetails, tripRecordId); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @GetMapping("/reviews/{tripRecordReviewId}") + public ResponseEntity> getTripRecordReview( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordReviewId + ) { + + SimpleTripRecordResponseDto responseDto = + tripRecordReviewService.getTripRecordReview(principalDetails, tripRecordReviewId); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @DeleteMapping("/reviews") + public ResponseEntity> removeTripRecordReviews( + @RequestBody DeleteTripRecordReviewRequestDto requestDto + ) { + + DeleteTripRecordReviewResponseDto responseDto = + tripRecordReviewService.deleteTripRecordReviews(requestDto); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @GetMapping("/{tripRecordId}/reviews") + public ResponseEntity> getTripRecordReviews( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId, + Pageable pageable + ) { + + TripRecordReviewListResponseDto responseDto = + tripRecordReviewService.getTripRecordReviewList(principalDetails, tripRecordId, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(responseDto)); + } + + @GetMapping("/reviews/my") + public ResponseEntity> getMyTripRecordReviews( + @AuthenticationPrincipal PrincipalDetails principalDetails, + Pageable pageable + ) { + + TripRecordReviewListResponseDto responseDtos = + tripRecordReviewService.getMyTripRecordReviewList(principalDetails, pageable); + return ResponseEntity.ok().body(ResponseDTO.okWithData(responseDtos)); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/controller/TripRecordReviewControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/controller/TripRecordReviewControllerAdvice.java new file mode 100644 index 00000000..275b3bcc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/controller/TripRecordReviewControllerAdvice.java @@ -0,0 +1,41 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.controller; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.exception.TripRecordReviewAlreadyExistsException; +import com.haejwo.tripcometrue.domain.review.triprecordreview.exception.TripRecordReviewDeleteAllFailureException; +import com.haejwo.tripcometrue.domain.review.triprecordreview.exception.TripRecordReviewNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class TripRecordReviewControllerAdvice { + + @ExceptionHandler(TripRecordReviewAlreadyExistsException.class) + public ResponseEntity> handleTripRecordReviewAlreadyExistsException(TripRecordReviewAlreadyExistsException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(TripRecordReviewNotFoundException.class) + public ResponseEntity> handleTripRecordReviewNotFoundException(TripRecordReviewNotFoundException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(TripRecordReviewDeleteAllFailureException.class) + public ResponseEntity> handleTripRecordReviewDeleteAllFailureException(TripRecordReviewDeleteAllFailureException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/DeleteTripRecordReviewRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/DeleteTripRecordReviewRequestDto.java new file mode 100644 index 00000000..a4d974d6 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/DeleteTripRecordReviewRequestDto.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request; + +import java.util.List; + +public record DeleteTripRecordReviewRequestDto( + + List tripRecordReviewIds + +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/EvaluateTripRecordReviewRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/EvaluateTripRecordReviewRequestDto.java new file mode 100644 index 00000000..f7328722 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/EvaluateTripRecordReviewRequestDto.java @@ -0,0 +1,31 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import com.haejwo.tripcometrue.domain.review.triprecordreview.validation.HalfUnit; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; + +public record EvaluateTripRecordReviewRequestDto( + + @NotNull(message = "๋ณ„์ ์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "0.5", message = "0.5์  ์•„๋ž˜๋กœ ํ‰๊ฐ€ํ•  ์ˆœ ์—†์Šต๋‹ˆ๋‹ค.") + @Max(value = 5, message = "5์ ์„ ์ดˆ๊ณผํ•  ์ˆœ ์—†์Šต๋‹ˆ๋‹ค.") + @HalfUnit(message = "๋ณ„์ ์€ 0.5 ๋‹จ์œ„๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + Float ratingScore + +) { + + public TripRecordReview toEntity(Member member, TripRecord tripRecord) { + return TripRecordReview.builder() + .member(member) + .tripRecord(tripRecord) + .ratingScore(this.ratingScore) + .build(); + } +} + + + diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/ModifyTripRecordReviewRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/ModifyTripRecordReviewRequestDto.java new file mode 100644 index 00000000..a64c9061 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/ModifyTripRecordReviewRequestDto.java @@ -0,0 +1,26 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.validation.HalfUnit; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.Length; + +public record ModifyTripRecordReviewRequestDto( + + @NotNull(message = "๋ณ„์ ์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.") + @DecimalMin(value = "0.5", message = "0.5์  ์•„๋ž˜๋กœ ํ‰๊ฐ€ํ•  ์ˆœ ์—†์Šต๋‹ˆ๋‹ค.") + @Max(value = 5, message = "5์ ์„ ์ดˆ๊ณผํ•  ์ˆœ ์—†์Šต๋‹ˆ๋‹ค.") + @HalfUnit(message = "๋ณ„์ ์€ 0.5 ๋‹จ์œ„๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + Float ratingScore, + + @NotBlank + @Length(min = 10, max = 2000, message = "์ž‘์„ฑ ํ—ˆ์šฉ ๋ฒ”์œ„๋Š” ์ตœ์†Œ 10์ž ๋˜๋Š” ์ตœ๋Œ€ 2,000์ž ์ž…๋‹ˆ๋‹ค.") + String content, + + String imageUrl + +) { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/RegisterTripRecordReviewRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/RegisterTripRecordReviewRequestDto.java new file mode 100644 index 00000000..ba5b161d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/request/RegisterTripRecordReviewRequestDto.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request; + +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.Length; + +public record RegisterTripRecordReviewRequestDto( + + @NotBlank + @Length(min = 10, max = 2000, message = "์ž‘์„ฑ ํ—ˆ์šฉ ๋ฒ”์œ„๋Š” ์ตœ์†Œ 10์ž ๋˜๋Š” ์ตœ๋Œ€ 2,000์ž ์ž…๋‹ˆ๋‹ค.") + String content, + + String imageUrl + +) { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/EvaluateTripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/EvaluateTripRecordReviewResponseDto.java new file mode 100644 index 00000000..efb60dc1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/EvaluateTripRecordReviewResponseDto.java @@ -0,0 +1,17 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; + +public record EvaluateTripRecordReviewResponseDto( + + Long tripRecordReviewId, + Float ratingScore + +) { + public static EvaluateTripRecordReviewResponseDto fromEntity(TripRecordReview tripRecordReview) { + return new EvaluateTripRecordReviewResponseDto( + tripRecordReview.getId(), + tripRecordReview.getRatingScore() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/SimpleTripRecordResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/SimpleTripRecordResponseDto.java new file mode 100644 index 00000000..1b0332ac --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/SimpleTripRecordResponseDto.java @@ -0,0 +1,22 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; + +public record SimpleTripRecordResponseDto( + + String tripRecordTitle, + String imageUrl, + String content, + Float ratingScore + +) { + + public static SimpleTripRecordResponseDto fromEntity(String title, TripRecordReview tripRecordReview) { + return new SimpleTripRecordResponseDto( + title, + tripRecordReview.getImageUrl(), + tripRecordReview.getContent(), + tripRecordReview.getRatingScore() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/TripRecordReviewListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/TripRecordReviewListResponseDto.java new file mode 100644 index 00000000..06445dfa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/TripRecordReviewListResponseDto.java @@ -0,0 +1,29 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record TripRecordReviewListResponseDto( + + Long totalCount, + int nowPageNumber, + boolean isFirst, + boolean isLast, + List tripRecordReviews + +) { + public static TripRecordReviewListResponseDto fromResponseDtos( + Page reviews, + List tripRecordReviews + ) { + return new TripRecordReviewListResponseDto( + reviews.getTotalElements(), + reviews.getNumber(), + reviews.isFirst(), + reviews.isLast(), + tripRecordReviews + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/TripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/TripRecordReviewResponseDto.java new file mode 100644 index 00000000..35d2db2d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/TripRecordReviewResponseDto.java @@ -0,0 +1,40 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; + +import java.time.LocalDateTime; + +public record TripRecordReviewResponseDto( + + Long tripRecordId, + String tripRecordTitle, + Long tripRecordReviewId, + String imageUrl, + Long memberId, + String nickname, + Float ratingScore, + String content, + Integer likeCount, + boolean amILike, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt + +) { + public static TripRecordReviewResponseDto fromEntity(TripRecordReview tripRecordReview, boolean amILike) { + return new TripRecordReviewResponseDto( + tripRecordReview.getTripRecord().getId(), + tripRecordReview.getTripRecord().getTitle(), + tripRecordReview.getId(), + tripRecordReview.getImageUrl(), + tripRecordReview.getMember().getId(), + tripRecordReview.getMember().getMemberBase().getNickname(), + tripRecordReview.getRatingScore(), + tripRecordReview.getContent(), + tripRecordReview.getLikeCount(), + amILike, + tripRecordReview.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteAllSuccessTripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteAllSuccessTripRecordReviewResponseDto.java new file mode 100644 index 00000000..cc4a8d19 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteAllSuccessTripRecordReviewResponseDto.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete; + +import lombok.Getter; + +@Getter +public class DeleteAllSuccessTripRecordReviewResponseDto implements DeleteTripRecordReviewResponseDto { + + private final String successMessage = "์„ฑ๊ณต์ ์œผ๋กœ ๋ชจ๋“  ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ(๋“ค)์„ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค."; +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteSomeFailureTripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteSomeFailureTripRecordReviewResponseDto.java new file mode 100644 index 00000000..1d33aa1b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteSomeFailureTripRecordReviewResponseDto.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class DeleteSomeFailureTripRecordReviewResponseDto implements DeleteTripRecordReviewResponseDto { + + private final String errorMessage = "์‚ญ์ œ์— ์‹คํŒจํ•œ ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ(๋“ค)์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."; + private final List failedTripRecordReviewIds; + + public DeleteSomeFailureTripRecordReviewResponseDto(List failedIds) { + this.failedTripRecordReviewIds = failedIds; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteTripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteTripRecordReviewResponseDto.java new file mode 100644 index 00000000..b637c5db --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/delete/DeleteTripRecordReviewResponseDto.java @@ -0,0 +1,4 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete; + +public interface DeleteTripRecordReviewResponseDto { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/EmptyTripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/EmptyTripRecordReviewResponseDto.java new file mode 100644 index 00000000..65d92d66 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/EmptyTripRecordReviewResponseDto.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest; + +public record EmptyTripRecordReviewResponseDto( + + Long totalCount, + Long myTripRecordReviewId, + Float myRatingScore, + boolean canRegisterContent + +) implements LatestReviewResponseDto { + public static EmptyTripRecordReviewResponseDto fromData(Long totalCount, Long myTripRecordReviewId, Float myRatingScore, boolean canRegisterContent) { + return new EmptyTripRecordReviewResponseDto(totalCount, myTripRecordReviewId, myRatingScore, canRegisterContent); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/LatestReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/LatestReviewResponseDto.java new file mode 100644 index 00000000..9fcaab95 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/LatestReviewResponseDto.java @@ -0,0 +1,4 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest; + +public interface LatestReviewResponseDto { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/LatestTripRecordReviewResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/LatestTripRecordReviewResponseDto.java new file mode 100644 index 00000000..2ff5f16f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/dto/response/latest/LatestTripRecordReviewResponseDto.java @@ -0,0 +1,28 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.TripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; + +public record LatestTripRecordReviewResponseDto( + + Long totalCount, + TripRecordReviewResponseDto latestTripRecordReview, + Long myTripRecordReviewId, + Float myRatingScore, + boolean canRegisterContent + +) implements LatestReviewResponseDto { + public static LatestTripRecordReviewResponseDto fromEntity( + Long totalCount, + TripRecordReview tripRecordReview, + Long myTripRecordReviewId, + Float myRatingScore, + boolean canRegisterContent) { + return new LatestTripRecordReviewResponseDto( + totalCount, + TripRecordReviewResponseDto.fromEntity(tripRecordReview, false), + myTripRecordReviewId, + myRatingScore, + canRegisterContent); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/entity/TripRecordReview.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/entity/TripRecordReview.java new file mode 100644 index 00000000..846ee42f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/entity/TripRecordReview.java @@ -0,0 +1,105 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.entity; + +import com.haejwo.tripcometrue.domain.likes.entity.TripRecordReviewLikes; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.ModifyTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.RegisterTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static com.haejwo.tripcometrue.domain.review.global.PointType.ONLY_ONE_POINT; +import static com.haejwo.tripcometrue.domain.review.global.PointType.TWO_POINTS; +import static jakarta.persistence.CascadeType.REMOVE; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordReview extends BaseTimeEntity { + + @Id + @GeneratedValue + @Column(name = "trip_record_review_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @OneToMany(mappedBy = "tripRecordReview", cascade = REMOVE, orphanRemoval = true) + private List tripRecordReviewLikeses = new ArrayList<>(); + + @NotNull + private Float ratingScore; + + @Lob + private String content; + + private Integer likeCount; + private String imageUrl; + private boolean hasAnyRegisteredPhotoUrl; + + @Builder + public TripRecordReview( + Member member, TripRecord tripRecord, String content, + Float ratingScore, Integer likeCount, String imageUrl, + boolean hasAnyRegisteredPhotoUrl + ) { + this.member = member; + this.tripRecord = tripRecord; + this.content = content; + this.ratingScore = ratingScore; + this.likeCount = likeCount; + this.imageUrl = imageUrl; + this.hasAnyRegisteredPhotoUrl = hasAnyRegisteredPhotoUrl; + } + + public void registerContent(RegisterTripRecordReviewRequestDto requestDto, Member member) { + this.content = requestDto.content(); + + if (requestDto.imageUrl() != null) { + this.imageUrl = requestDto.imageUrl(); + member.earnPoint(TWO_POINTS.getPoint()); + hasAnyRegisteredPhotoUrl = true; + return; + } + + member.earnPoint(ONLY_ONE_POINT.getPoint()); + } + + public void update(ModifyTripRecordReviewRequestDto requestDto, Member member) { + this.ratingScore = requestDto.ratingScore(); + this.content = requestDto.content(); + + if (!hasAnyRegisteredPhotoUrl && requestDto.imageUrl() != null) { + member.earnPoint(ONLY_ONE_POINT.getPoint()); + hasAnyRegisteredPhotoUrl = true; + } + this.imageUrl = requestDto.imageUrl(); + } + + public void increaseLikesCount() { + this.likeCount += 1; + } + + public void decreaseLikesCount() { + this.likeCount -= 1; + } + + @PrePersist + private void init() { + likeCount = 0; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/ContentNotInitializedException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/ContentNotInitializedException.java new file mode 100644 index 00000000..267e2405 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/ContentNotInitializedException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class ContentNotInitializedException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.CAN_NOT_MODIFYING_WITHOUT_CONTENT; + + public ContentNotInitializedException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/DuplicateTripRecordReviewException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/DuplicateTripRecordReviewException.java new file mode 100644 index 00000000..43e288a9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/DuplicateTripRecordReviewException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class DuplicateTripRecordReviewException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.REGISTERING_DUPLICATE_TRIP_RECORD_REVIEW; + + public DuplicateTripRecordReviewException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewAlreadyExistsException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewAlreadyExistsException.java new file mode 100644 index 00000000..b636c081 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewAlreadyExistsException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripRecordReviewAlreadyExistsException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_RECORD_REVIEW_ALREADY_EXISTS; + + public TripRecordReviewAlreadyExistsException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewDeleteAllFailureException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewDeleteAllFailureException.java new file mode 100644 index 00000000..4b380b41 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewDeleteAllFailureException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripRecordReviewDeleteAllFailureException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_RECORD_REVIEW_DELETE_ALL_FAILURE; + + public TripRecordReviewDeleteAllFailureException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewNotFoundException.java new file mode 100644 index 00000000..09f7051a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/exception/TripRecordReviewNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripRecordReviewNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_RECORD_REVIEW_NOT_FOUND; + + public TripRecordReviewNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepository.java new file mode 100644 index 00000000..56b812c5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepository.java @@ -0,0 +1,31 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.repository; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TripRecordReviewRepository extends JpaRepository, TripRecordReviewRepositoryCustom { + + @Query("select trr from TripRecordReview trr join fetch trr.member m where trr.member = :member and trr.content is not null order by trr.createdAt desc") + Page findByMember(@Param("member") Member member, Pageable pageable); + + @Query("select trr from TripRecordReview trr where trr.member = :member and trr.tripRecord.id = :tripRecordId") + Optional findByMemberAndTripRecordId(@Param("member") Member member, @Param("tripRecordId") Long tripRecordId); + + @Query("select trr from TripRecordReview trr where trr.tripRecord.id = :tripRecordId and trr.content is not null order by trr.createdAt desc limit 1") + Optional findTopByTripRecordIdOrderByCreatedAtDesc(@Param("tripRecordId") Long tripRecordId); + + boolean existsByMemberAndTripRecord(Member member, TripRecord tripRecord); + + @Query("select count(trr) from TripRecordReview trr where trr.content is not null") + Long countByTripRecordId(Long tripRecordId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepositoryCustom.java new file mode 100644 index 00000000..6756835d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.repository; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface TripRecordReviewRepositoryCustom { + + Page findByTripRecord(TripRecord tripRecord, Pageable pageable); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepositoryCustomImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepositoryCustomImpl.java new file mode 100644 index 00000000..15371a5c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/repository/TripRecordReviewRepositoryCustomImpl.java @@ -0,0 +1,61 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.repository; + +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.List; + +import static com.haejwo.tripcometrue.domain.review.triprecordreview.entity.QTripRecordReview.tripRecordReview; + +public class TripRecordReviewRepositoryCustomImpl implements TripRecordReviewRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public TripRecordReviewRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findByTripRecord(TripRecord tripRecord, Pageable pageable) { + + List content = queryFactory + .selectFrom(tripRecordReview) + .join(tripRecordReview.tripRecord).on(tripRecordReview.tripRecord.id.eq(tripRecord.getId())) + .where(tripRecordReview.content.isNotNull()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(getOrderSpecifier(pageable.getSort())) + .fetch(); + + JPAQuery count = queryFactory + .select(tripRecordReview.count()) + .from(tripRecordReview) + .join(tripRecordReview.tripRecord).on(tripRecordReview.tripRecord.id.eq(tripRecord.getId())) + .where(tripRecordReview.content.isNotNull() + ); + + return PageableExecutionUtils.getPage(content, pageable, count::fetchOne); + } + + private OrderSpecifier[] getOrderSpecifier(Sort sort) { + return sort.stream() + .map(order -> { + PathBuilder entityPath = new PathBuilder<>(TripRecordReview.class, "tripRecordReview"); + String property = order.getProperty(); + OrderSpecifier orderSpecifier = order.isAscending() ? + entityPath.getString(property).asc() : + entityPath.getString(property).desc(); + return orderSpecifier; + }) + .toArray(OrderSpecifier[]::new); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/service/TripRecordReviewService.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/service/TripRecordReviewService.java new file mode 100644 index 00000000..4886a19c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/service/TripRecordReviewService.java @@ -0,0 +1,284 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.service; + +import com.haejwo.tripcometrue.domain.alarm.entity.AlarmType; +import com.haejwo.tripcometrue.domain.alarm.service.AlarmService; +import com.haejwo.tripcometrue.domain.likes.entity.TripRecordReviewLikes; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.exception.UserInvalidAccessException; +import com.haejwo.tripcometrue.domain.member.exception.UserNotFoundException; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.DeleteTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.EvaluateTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.ModifyTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.request.RegisterTripRecordReviewRequestDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.EvaluateTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.SimpleTripRecordResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.TripRecordReviewListResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.TripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete.DeleteAllSuccessTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete.DeleteSomeFailureTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.delete.DeleteTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest.EmptyTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest.LatestReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.dto.response.latest.LatestTripRecordReviewResponseDto; +import com.haejwo.tripcometrue.domain.review.triprecordreview.entity.TripRecordReview; +import com.haejwo.tripcometrue.domain.review.triprecordreview.exception.*; +import com.haejwo.tripcometrue.domain.review.triprecordreview.repository.TripRecordReviewRepository; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import com.haejwo.tripcometrue.domain.tripplan.repository.TripPlanRepository; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TripRecordReviewService { + + private final TripRecordReviewRepository tripRecordReviewRepository; + private final TripRecordRepository tripRecordRepository; + private final MemberRepository memberRepository; + private final TripPlanRepository tripPlanRepository; + private final AlarmService alarmService; + + // FIXME: 1/18/24 ratingScore @NotNull๊ณผ ์ƒ์ถฉ๋˜๋Š” ๋ถ€๋ถ„ ์ˆ˜์ •ํ•˜๊ธฐ + @Transactional + public EvaluateTripRecordReviewResponseDto saveRatingScore( + PrincipalDetails principalDetails, + Long tripRecordId, + EvaluateTripRecordReviewRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + TripRecord tripRecord = getTripRecordById(tripRecordId); + + isAlreadyTripRecordReviewExists(loginMember, tripRecord); + + return EvaluateTripRecordReviewResponseDto + .fromEntity(tripRecordReviewRepository.save(requestDto.toEntity(loginMember, tripRecord))); + } + + private Member getMember(PrincipalDetails principalDetails) { + if (principalDetails == null) { + return null; + } + return memberRepository.findById(principalDetails.getMember().getId()) + .orElseThrow(UserNotFoundException::new); + } + + private void isAlreadyTripRecordReviewExists(Member member, TripRecord tripRecord) { + if (tripRecordReviewRepository.existsByMemberAndTripRecord(member, tripRecord)) { + throw new TripRecordReviewAlreadyExistsException(); + } + } + + private TripRecord getTripRecordById(Long tripRecordId) { + return tripRecordRepository.findById(tripRecordId) + .orElseThrow(TripRecordNotFoundException::new); + } + + // FIXME: 1/18/24 ratingScore @NotNull๊ณผ ์ƒ์ถฉ๋˜๋Š” ๋ถ€๋ถ„ ์ˆ˜์ •ํ•˜๊ธฐ + @Transactional + public void modifyTripRecordReview( + PrincipalDetails principalDetails, + Long tripRecordReviewId, + ModifyTripRecordReviewRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + TripRecordReview tripRecordReview = getTripRecordReviewById(tripRecordReviewId); + + validateRightMemberAccess(loginMember, tripRecordReview); + isContentAlreadyRegistered(tripRecordReview); + + tripRecordReview.update(requestDto, loginMember); + } + + private TripRecordReview getTripRecordReviewById(Long tripRecordReviewId) { + return tripRecordReviewRepository.findById(tripRecordReviewId) + .orElseThrow(TripRecordReviewNotFoundException::new); + } + + private void validateRightMemberAccess(Member member, TripRecordReview tripRecordReview) { + if (!Objects.equals(tripRecordReview.getMember().getId(), member.getId())) { + throw new UserInvalidAccessException(); + } + } + + private void isContentAlreadyRegistered(TripRecordReview tripRecordReview) { + if (tripRecordReview.getContent() == null) { + throw new ContentNotInitializedException(); + } + } + + private boolean hasLikedTripRecordReview(PrincipalDetails principalDetails, TripRecordReview tripRecordReview) { + List memberIds = tripRecordReview.getTripRecordReviewLikeses().stream() + .map(TripRecordReviewLikes::getMember) + .map(Member::getId) + .toList(); + return memberIds.contains(principalDetails.getMember().getId()); + } + + @Transactional + public void registerContent( + PrincipalDetails principalDetails, + Long tripRecordReviewId, + RegisterTripRecordReviewRequestDto requestDto + ) { + + Member loginMember = getMember(principalDetails); + TripRecordReview tripRecordReview = getTripRecordReviewById(tripRecordReviewId); + + validateRightMemberAccess(loginMember, tripRecordReview); + isReviewAlreadyRegister(tripRecordReview); + + tripRecordReview.registerContent(requestDto, loginMember); + + alarmService.addAlarm( + loginMember, + tripRecordReview.getTripRecord().getMember(), + AlarmType.NEW_TRIP_RECORD_REVIEW, + tripRecordReview.getTripRecord().getId(), + tripRecordReviewId); + } + + private void isReviewAlreadyRegister(TripRecordReview tripRecordReview) { + if (tripRecordReview.getContent() != null) { + throw new DuplicateTripRecordReviewException(); + } + } + + @Transactional + public DeleteTripRecordReviewResponseDto deleteTripRecordReviews( + DeleteTripRecordReviewRequestDto requestDto + ) { + + List tripRecordReviewIds = requestDto.tripRecordReviewIds(); + List failedIds = new ArrayList<>(); + + tripRecordReviewIds.forEach(tripRecordReviewId -> { + if (tripRecordReviewRepository.existsById(tripRecordReviewId)) { + tripRecordReviewRepository.deleteById(tripRecordReviewId); + } else { + failedIds.add(tripRecordReviewId); + } + }); + + if (isDeleteAllFail(tripRecordReviewIds, failedIds)) { + throw new TripRecordReviewDeleteAllFailureException(); + } + if (!failedIds.isEmpty()) { + return new DeleteSomeFailureTripRecordReviewResponseDto(failedIds); + } + + return new DeleteAllSuccessTripRecordReviewResponseDto(); + } + + private boolean isDeleteAllFail(List tripRecordReviewIds, List failedIds) { + return tripRecordReviewIds.size() == failedIds.size(); + } + + public LatestReviewResponseDto getLatestReview(PrincipalDetails principalDetails, Long tripRecordId) { + + getTripRecordById(tripRecordId); + Member loginMember = getMember(principalDetails); + + Optional latestReview = tripRecordReviewRepository + .findTopByTripRecordIdOrderByCreatedAtDesc(tripRecordId); + + Long totalCount = tripRecordReviewRepository.countByTripRecordId(tripRecordId); + Optional myReview = tripRecordReviewRepository + .findByMemberAndTripRecordId(loginMember, tripRecordId); + + String content = myReview.map(TripRecordReview::getContent).orElse(null); + Long myReviewId = myReview.map(TripRecordReview::getId).orElse(null); + Float myRatingScore = myReview.map(TripRecordReview::getRatingScore).orElse(0f); + + boolean isReviewable = canWriteReview(loginMember, tripRecordId, content, myRatingScore); + + if (latestReview.isEmpty()) { + return EmptyTripRecordReviewResponseDto.fromData(totalCount, myReviewId, myRatingScore, isReviewable); + } + return LatestTripRecordReviewResponseDto.fromEntity(totalCount, latestReview.get(), myReviewId, myRatingScore, isReviewable); + } + + private boolean canWriteReview(Member loginMember, Long tripRecordId, String content, float score) { + + if (content != null || score == 0f) { + return false; + } + + Optional latestTripPlan = tripPlanRepository.findByMemberIdAndTripRecordId(loginMember, tripRecordId); + return latestTripPlan.map(tripPlan -> + tripPlan.getTripEndDay().isBefore(LocalDate.now()) + ) + .orElse(false); + } + + public SimpleTripRecordResponseDto getTripRecordReview(PrincipalDetails principalDetails, Long tripRecordReviewId) { + + Member loginMember = getMember(principalDetails); + TripRecordReview tripRecordReview = tripRecordReviewRepository.findById(tripRecordReviewId) + .orElseThrow(TripRecordReviewNotFoundException::new); + + validateRightMemberAccess(loginMember, tripRecordReview); + isContentAlreadyRegistered(tripRecordReview); + + String title = tripRecordReview.getTripRecord().getTitle(); + return SimpleTripRecordResponseDto.fromEntity(title, tripRecordReview); + } + + public TripRecordReviewListResponseDto getTripRecordReviewList( + PrincipalDetails principalDetails, + Long tripRecordId, + Pageable pageable + ) { + + TripRecord tripRecord = getTripRecordById(tripRecordId); + Page reviews = tripRecordReviewRepository.findByTripRecord(tripRecord, pageable); + + return TripRecordReviewListResponseDto.fromResponseDtos( + reviews, + reviews.map(tripRecordReview -> { + boolean hasLiked = false; + if (principalDetails != null) { + hasLiked = hasLikedTripRecordReview(principalDetails, tripRecordReview); + } + return TripRecordReviewResponseDto.fromEntity( + tripRecordReview, + hasLiked + ); + } + ).toList()); + } + + public TripRecordReviewListResponseDto getMyTripRecordReviewList( + PrincipalDetails principalDetails, + Pageable pageable + ) { + + Member loginMember = getMember(principalDetails); + Page reviews = tripRecordReviewRepository.findByMember(loginMember, pageable); + + return TripRecordReviewListResponseDto.fromResponseDtos( + reviews, + reviews.map(tripRecordReview -> TripRecordReviewResponseDto.fromEntity( + tripRecordReview, + hasLikedTripRecordReview(principalDetails, tripRecordReview)) + ).toList()); + } + + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/validation/HalfUnit.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/validation/HalfUnit.java new file mode 100644 index 00000000..7297016f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/validation/HalfUnit.java @@ -0,0 +1,22 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) // ๋ณ€์ˆ˜์— ์‚ฌ์šฉ +@Retention(RetentionPolicy.RUNTIME) // ์—๋…ธํ…Œ์ด์…˜ ์œ ์ง€๋ฒ”์œ„ +@Constraint(validatedBy = HalfUnitValidator.class) // ๊ฒ€์ฆ ํด๋ž˜์Šค +public @interface HalfUnit { + + String message() default ""; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/validation/HalfUnitValidator.java b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/validation/HalfUnitValidator.java new file mode 100644 index 00000000..653030d0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/review/triprecordreview/validation/HalfUnitValidator.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.review.triprecordreview.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class HalfUnitValidator implements ConstraintValidator { + + @Override + public boolean isValid(Float ratingScore, ConstraintValidatorContext context) { + return ratingScore % 0.5 == 0; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/controller/StoreController.java b/src/main/java/com/haejwo/tripcometrue/domain/store/controller/StoreController.java new file mode 100644 index 00000000..dd0ad4e5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/controller/StoreController.java @@ -0,0 +1,135 @@ +package com.haejwo.tripcometrue.domain.store.controller; + +import com.haejwo.tripcometrue.domain.store.dto.request.CityStoreRequestDto; +import com.haejwo.tripcometrue.domain.store.dto.request.PlaceStoreRequestDto; +import com.haejwo.tripcometrue.domain.store.dto.request.TripRecordStoreRequestDto; +import com.haejwo.tripcometrue.domain.store.dto.response.CheckCityStoredResponseDto; +import com.haejwo.tripcometrue.domain.store.dto.response.CityStoreResponseDto; +import com.haejwo.tripcometrue.domain.store.dto.response.PlaceStoreResponseDto; +import com.haejwo.tripcometrue.domain.store.dto.response.TripRecordStoreResponseDto; +import com.haejwo.tripcometrue.domain.store.service.StoreService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequiredArgsConstructor +public class StoreController { + + private final StoreService storeService; + + @PostMapping("/v1/cities/stores") + public ResponseEntity> storeCity( + @RequestBody CityStoreRequestDto request, + @AuthenticationPrincipal PrincipalDetails principalDetails) { + CityStoreResponseDto response = storeService.storeCity(request, principalDetails); + return ResponseEntity.ok(ResponseDTO.okWithData(response)); + } + + @PostMapping("/v1/places/stores") + public ResponseEntity> storePlace( + @RequestBody PlaceStoreRequestDto request, + @AuthenticationPrincipal PrincipalDetails principalDetails) { + PlaceStoreResponseDto response = storeService.storePlace(request, principalDetails); + return ResponseEntity.ok(ResponseDTO.okWithData(response)); + } + + @PostMapping("/v1/trip-records/stores") + public ResponseEntity> storeTripRecord( + @RequestBody TripRecordStoreRequestDto request, + @AuthenticationPrincipal PrincipalDetails principalDetails) { + TripRecordStoreResponseDto response = storeService.storeTripRecord(request, principalDetails); + return ResponseEntity.ok(ResponseDTO.okWithData(response)); + } + + @DeleteMapping("/v1/cities/{cityId}/stores") + public ResponseEntity> unstoreCity(@PathVariable Long cityId, + @AuthenticationPrincipal PrincipalDetails principalDetails) { + storeService.unstoreCity(principalDetails, cityId); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @DeleteMapping("/v1/places/{placeId}/stores") + public ResponseEntity> unstorePlace(@PathVariable Long placeId, + @AuthenticationPrincipal PrincipalDetails principalDetails) { + storeService.unstorePlace(principalDetails, placeId); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @DeleteMapping("/v1/trip-records/{tripRecordId}/stores") + public ResponseEntity> unstoreTripRecord(@PathVariable Long tripRecordId, + @AuthenticationPrincipal PrincipalDetails principalDetails) { + storeService.unstoreTripRecord(principalDetails, tripRecordId); + return ResponseEntity.ok(ResponseDTO.ok()); + } + + @GetMapping("/v1/cities/stores") + public ResponseEntity>> getStoredCities( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + Page storedCities = storeService.getStoredCities(principalDetails, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(storedCities)); + } + + @GetMapping("/v1/places/stores") + public ResponseEntity>> getStoredPlaces( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + Page storedPlaces = storeService.getStoredPlaces(principalDetails, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(storedPlaces)); + } + + @GetMapping("/v1/trip-records/stores") + public ResponseEntity>> getStoredTripRecords( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + Page storedTripRecords = storeService.getStoredTripRecords(principalDetails, pageable); + return ResponseEntity.ok(ResponseDTO.okWithData(storedTripRecords)); + } + + @GetMapping("/v1/cities/{cityId}/stores/count") + public ResponseEntity> getStoredCountForCity(@PathVariable Long cityId) { + Long count = storeService.getStoredCountForCity(cityId); + return ResponseEntity.ok(ResponseDTO.okWithData(count)); + } + + @GetMapping("/v1/places/{placeId}/stores/count") + public ResponseEntity> getStoredCountForPlace(@PathVariable Long placeId) { + Long count = storeService.getStoredCountForPlace(placeId); + return ResponseEntity.ok(ResponseDTO.okWithData(count)); + } + + @GetMapping("/v1/trip-records/{tripRecordId}/stores/count") + public ResponseEntity> getStoredCountForTripRecord(@PathVariable Long tripRecordId) { + Long count = storeService.getStoredCountForTripRecord(tripRecordId); + return ResponseEntity.ok(ResponseDTO.okWithData(count)); + } + + @GetMapping("/v1/cities/{cityId}/stores") + public ResponseEntity> checkCityStoredByLoginMember( + @PathVariable("cityId") Long cityId, + @AuthenticationPrincipal PrincipalDetails principalDetails + ) { + + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + storeService.checkCityStoredByLoginMember(principalDetails, cityId) + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/controller/StoreControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/store/controller/StoreControllerAdvice.java new file mode 100644 index 00000000..ebaaecdb --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/controller/StoreControllerAdvice.java @@ -0,0 +1,32 @@ +package com.haejwo.tripcometrue.domain.store.controller; +import com.haejwo.tripcometrue.domain.store.exception.StoreAlreadyExistException; +import com.haejwo.tripcometrue.domain.store.exception.StoreNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + + +@RestControllerAdvice +public class StoreControllerAdvice { + @ExceptionHandler(StoreNotFoundException.class) + public ResponseEntity> StoreNotFoundExceptionHandler(StoreNotFoundException e) { + + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(StoreAlreadyExistException.class) + public ResponseEntity> StoreAlreadyExistExceptionHandler(StoreAlreadyExistException e) { + + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/CityStoreRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/CityStoreRequestDto.java new file mode 100644 index 00000000..7f80ef26 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/CityStoreRequestDto.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.store.dto.request; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.store.entity.CityStore; + + +public record CityStoreRequestDto(Long cityId) { + + public CityStore toEntity(Member member, City city) { + return CityStore.builder() + .member(member) + .city(city) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/PlaceStoreRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/PlaceStoreRequestDto.java new file mode 100644 index 00000000..4583cfc9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/PlaceStoreRequestDto.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.store.dto.request; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.store.entity.PlaceStore; + + +public record PlaceStoreRequestDto(Long placeId) { + + public PlaceStore toEntity(Member member, Place place) { + return PlaceStore.builder() + .member(member) + .place(place) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/TripRecordStoreRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/TripRecordStoreRequestDto.java new file mode 100644 index 00000000..6c76708b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/request/TripRecordStoreRequestDto.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.store.dto.request; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.store.entity.TripRecordStore; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; + + +public record TripRecordStoreRequestDto(Long tripRecordId) { + + public TripRecordStore toEntity(Member member, TripRecord tripRecord) { + return TripRecordStore.builder() + .member(member) + .tripRecord(tripRecord) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/CheckCityStoredResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/CheckCityStoredResponseDto.java new file mode 100644 index 00000000..5ccd0baf --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/CheckCityStoredResponseDto.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.store.dto.response; + +import lombok.Builder; + +public record CheckCityStoredResponseDto( + boolean isStored +) { + + @Builder + public CheckCityStoredResponseDto { + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/CityStoreResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/CityStoreResponseDto.java new file mode 100644 index 00000000..36f8dea6 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/CityStoreResponseDto.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.store.dto.response; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.store.entity.CityStore; +import com.haejwo.tripcometrue.global.enums.Country; + +public record CityStoreResponseDto( + Long id, + String name, + Integer storeCount, + String imageUrl +) { + + public static CityStoreResponseDto fromEntity(CityStore cityStore) { + City city = cityStore.getCity(); + return new CityStoreResponseDto( + city.getId(), + city.getName(), + city.getStoreCount(), + city.getImageUrl() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/PlaceStoreResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/PlaceStoreResponseDto.java new file mode 100644 index 00000000..0d274166 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/PlaceStoreResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.store.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.store.entity.PlaceStore; +import jakarta.persistence.criteria.CriteriaBuilder.In; +import java.time.LocalTime; + +public record PlaceStoreResponseDto( + Long id, + String name, + Integer commentCount, + Integer storedCount, + String imageUrl +) { + + public static PlaceStoreResponseDto fromEntity(PlaceStore placeStore, String imageUrl) { + Place place = placeStore.getPlace(); + return new PlaceStoreResponseDto( + place.getId(), + place.getName(), + place.getCommentCount(), + place.getStoredCount(), + imageUrl + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/TripRecordStoreResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/TripRecordStoreResponseDto.java new file mode 100644 index 00000000..397f1b5f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/dto/response/TripRecordStoreResponseDto.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.store.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.store.entity.TripRecordStore; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import java.time.LocalDate; + +public record TripRecordStoreResponseDto( + Long id, + String title, + Integer storeCount, + String imageUrl +) { + + public static TripRecordStoreResponseDto fromEntity(TripRecordStore tripRecordStore, String imageUrl) { + TripRecord tripRecord = tripRecordStore.getTripRecord(); + return new TripRecordStoreResponseDto( + tripRecord.getId(), + tripRecord.getTitle(), + tripRecord.getStoreCount(), + imageUrl + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/entity/CityStore.java b/src/main/java/com/haejwo/tripcometrue/domain/store/entity/CityStore.java new file mode 100644 index 00000000..ba1f49b7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/entity/CityStore.java @@ -0,0 +1,35 @@ +package com.haejwo.tripcometrue.domain.store.entity; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class CityStore extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "city_store_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_id") + Member member; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "city_id") + City city; + + @Builder + private CityStore(Member member, City city){ + this.member = member; + this.city = city; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/entity/PlaceStore.java b/src/main/java/com/haejwo/tripcometrue/domain/store/entity/PlaceStore.java new file mode 100644 index 00000000..2e40f7df --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/entity/PlaceStore.java @@ -0,0 +1,35 @@ +package com.haejwo.tripcometrue.domain.store.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class PlaceStore extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "place_store_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_id") + Member member; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "place_id") + Place place; + + @Builder + private PlaceStore(Member member, Place place){ + this.member = member; + this.place = place; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/entity/TripRecordStore.java b/src/main/java/com/haejwo/tripcometrue/domain/store/entity/TripRecordStore.java new file mode 100644 index 00000000..0c1eaecc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/entity/TripRecordStore.java @@ -0,0 +1,42 @@ +package com.haejwo.tripcometrue.domain.store.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class TripRecordStore extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_store_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_Id") + Member member; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "trip_record_id") + TripRecord tripRecord; + + @Builder + private TripRecordStore(Member member, TripRecord tripRecord){ + this.member = member; + this.tripRecord = tripRecord; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/exception/StoreAlreadyExistException.java b/src/main/java/com/haejwo/tripcometrue/domain/store/exception/StoreAlreadyExistException.java new file mode 100644 index 00000000..5bac11a8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/exception/StoreAlreadyExistException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.store.exception; +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class StoreAlreadyExistException extends ApplicationException { + + + public StoreAlreadyExistException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/exception/StoreNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/store/exception/StoreNotFoundException.java new file mode 100644 index 00000000..499c10e8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/exception/StoreNotFoundException.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.store.exception; +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class StoreNotFoundException extends ApplicationException { + + public StoreNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/repository/CityStoreRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/store/repository/CityStoreRepository.java new file mode 100644 index 00000000..5f4af705 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/repository/CityStoreRepository.java @@ -0,0 +1,22 @@ +package com.haejwo.tripcometrue.domain.store.repository; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.store.entity.CityStore; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CityStoreRepository extends JpaRepository { + + Optional findByMemberAndCity(Member member, City city); + + Optional findByMemberIdAndCityId(Long memberId, Long cityId); + + Page findByMember(Member member, Pageable pageable); + + Long countByCity(City city); + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/repository/PlaceStoreRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/store/repository/PlaceStoreRepository.java new file mode 100644 index 00000000..694b53be --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/repository/PlaceStoreRepository.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.store.repository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.store.entity.PlaceStore; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PlaceStoreRepository extends JpaRepository { + + Optional findByMemberAndPlace(Member member, Place place); + + Optional findByMemberIdAndPlaceId(Long memberId, Long placeId); + + Page findByMember(Member member, Pageable pageable); + + Long countByPlace(Place place); + + Boolean existsByMemberAndPlace(Member member, Place place); + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/repository/TripRecordStoreRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/store/repository/TripRecordStoreRepository.java new file mode 100644 index 00000000..7e01e320 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/repository/TripRecordStoreRepository.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.store.repository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.store.entity.TripRecordStore; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TripRecordStoreRepository extends JpaRepository { + Optional findByMemberAndTripRecord(Member member, TripRecord tripRecord); + + Optional findByMemberIdAndTripRecordId(Long memberId, Long tripRecordId); + + Page findByMember(Member member, Pageable pageable); + + Long countByTripRecord(TripRecord tripRecord); + + Boolean existsByMemberAndTripRecord(Member member, TripRecord tripRecord); + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/store/service/StoreService.java b/src/main/java/com/haejwo/tripcometrue/domain/store/service/StoreService.java new file mode 100644 index 00000000..fb842999 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/store/service/StoreService.java @@ -0,0 +1,221 @@ +package com.haejwo.tripcometrue.domain.store.service; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.exception.PlaceNotFoundException; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.store.dto.request.CityStoreRequestDto; +import com.haejwo.tripcometrue.domain.store.dto.request.PlaceStoreRequestDto; +import com.haejwo.tripcometrue.domain.store.dto.request.TripRecordStoreRequestDto; +import com.haejwo.tripcometrue.domain.store.dto.response.CheckCityStoredResponseDto; +import com.haejwo.tripcometrue.domain.store.dto.response.CityStoreResponseDto; +import com.haejwo.tripcometrue.domain.store.dto.response.PlaceStoreResponseDto; +import com.haejwo.tripcometrue.domain.store.dto.response.TripRecordStoreResponseDto; +import com.haejwo.tripcometrue.domain.store.entity.CityStore; +import com.haejwo.tripcometrue.domain.store.entity.PlaceStore; +import com.haejwo.tripcometrue.domain.store.entity.TripRecordStore; +import com.haejwo.tripcometrue.domain.store.exception.StoreAlreadyExistException; +import com.haejwo.tripcometrue.domain.store.exception.StoreNotFoundException; +import com.haejwo.tripcometrue.domain.store.repository.CityStoreRepository; +import com.haejwo.tripcometrue.domain.store.repository.PlaceStoreRepository; +import com.haejwo.tripcometrue.domain.store.repository.TripRecordStoreRepository; +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleImageWithPlaceIdQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image.TripRecordScheduleImageRepository; +import com.haejwo.tripcometrue.global.exception.ErrorCode; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import java.util.List; +import java.util.Objects; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class StoreService{ + + private final CityRepository cityRepository; + private final PlaceRepository placeRepository; + private final TripRecordRepository tripRecordRepository; + private final CityStoreRepository cityStoreRepository; + private final PlaceStoreRepository placeStoreRepository; + private final TripRecordStoreRepository tripRecordStoreRepository; + private final TripRecordScheduleImageRepository tripRecordScheduleImageRepository; + + @Transactional + public CityStoreResponseDto storeCity(CityStoreRequestDto request, PrincipalDetails principalDetails) { + City city = cityRepository.findById(request.cityId()) + .orElseThrow(() -> new CityNotFoundException()); + + cityStoreRepository.findByMemberAndCity(principalDetails.getMember(), city) + .ifPresent(store -> { + throw new StoreAlreadyExistException(ErrorCode.STORE_ALREADY_EXIST); + }); + + city.incrementStoreCount(); + cityRepository.save(city); + + CityStore store = cityStoreRepository.save(request.toEntity(principalDetails.getMember(), city)); + return CityStoreResponseDto.fromEntity(store); + } + + @Transactional + public PlaceStoreResponseDto storePlace(PlaceStoreRequestDto request, PrincipalDetails principalDetails) { + Place place = placeRepository.findById(request.placeId()) + .orElseThrow(() -> new PlaceNotFoundException()); + + placeStoreRepository.findByMemberAndPlace(principalDetails.getMember(), place) + .ifPresent(store -> { + throw new StoreAlreadyExistException(ErrorCode.STORE_ALREADY_EXIST); + }); + + place.incrementStoreCount(); + placeRepository.save(place); + + PlaceStore store = placeStoreRepository.save(request.toEntity(principalDetails.getMember(), place)); + + String imageUrl = findFirstImageForPlace(place); + + return PlaceStoreResponseDto.fromEntity(store, imageUrl); + } + + @Transactional + public TripRecordStoreResponseDto storeTripRecord(TripRecordStoreRequestDto request, PrincipalDetails principalDetails) { + TripRecord tripRecord = tripRecordRepository.findById(request.tripRecordId()) + .orElseThrow(() -> new TripRecordNotFoundException()); + + tripRecordStoreRepository.findByMemberAndTripRecord(principalDetails.getMember(), tripRecord) + .ifPresent(store -> { + throw new StoreAlreadyExistException(ErrorCode.STORE_ALREADY_EXIST); + }); + + tripRecord.incrementStoreCount(); + tripRecordRepository.save(tripRecord); + + TripRecordStore store = tripRecordStoreRepository.save(request.toEntity(principalDetails.getMember(), tripRecord)); + List images = tripRecord.getTripRecordImages(); + String imageUrl = images.isEmpty() ? null : images.get(0).getImageUrl(); + + return TripRecordStoreResponseDto.fromEntity(store, imageUrl); + } + + @Transactional + public void unstoreCity(PrincipalDetails principalDetails, Long cityId) { + Long memberId = principalDetails.getMember().getId(); + CityStore cityStore = cityStoreRepository.findByMemberIdAndCityId(memberId, cityId) + .orElseThrow(() -> new StoreNotFoundException(ErrorCode.STORE_NOT_FOUND)); + + City city = cityRepository.findById(cityId) + .orElseThrow(); + city.decrementStoreCount(); + + cityStoreRepository.delete(cityStore); + } + + @Transactional + public void unstorePlace(PrincipalDetails principalDetails, Long placeId) { + Long memberId = principalDetails.getMember().getId(); + PlaceStore placeStore = placeStoreRepository.findByMemberIdAndPlaceId(memberId, placeId) + .orElseThrow(() -> new StoreNotFoundException(ErrorCode.STORE_NOT_FOUND)); + + Place place = placeRepository.findById(placeId) + .orElseThrow(()-> new PlaceNotFoundException()); + place.decrementStoreCount(); + + placeStoreRepository.delete(placeStore); + } + + @Transactional + public void unstoreTripRecord(PrincipalDetails principalDetails, Long tripRecordId) { + Long memberId = principalDetails.getMember().getId(); + TripRecordStore tripRecordStore = tripRecordStoreRepository.findByMemberIdAndTripRecordId(memberId, tripRecordId) + .orElseThrow(() -> new StoreNotFoundException(ErrorCode.STORE_NOT_FOUND)); + + TripRecord tripRecord = tripRecordRepository.findById(tripRecordId) + .orElseThrow(()-> new TripRecordNotFoundException()); + tripRecord.decrementStoreCount(); + + tripRecordStoreRepository.delete(tripRecordStore); + } + + @Transactional(readOnly = true) + public CheckCityStoredResponseDto checkCityStoredByLoginMember(PrincipalDetails principalDetails, Long cityId) { + + if (Objects.isNull(principalDetails.getMember())) { + return CheckCityStoredResponseDto.builder() + .isStored(false) + .build(); + } + + Long memberId = principalDetails.getMember().getId(); + + return CheckCityStoredResponseDto.builder() + .isStored( + cityStoreRepository + .findByMemberIdAndCityId(memberId, cityId) + .isPresent() + ) + .build(); + } + + public Page getStoredCities(PrincipalDetails principalDetails, Pageable pageable) { + Page storedCities = cityStoreRepository.findByMember(principalDetails.getMember(), pageable); + return storedCities.map(CityStoreResponseDto::fromEntity); + } + + public Page getStoredPlaces(PrincipalDetails principalDetails, Pageable pageable) { + Page storedPlaces = placeStoreRepository.findByMember(principalDetails.getMember(), pageable); + return storedPlaces.map(placeStore -> { + String imageUrl = findFirstImageForPlace(placeStore.getPlace()); + return PlaceStoreResponseDto.fromEntity(placeStore, imageUrl); + }); } + + private String findFirstImageForPlace(Place place){ + List images = + tripRecordScheduleImageRepository.findInPlaceIdsOrderByCreatedAtDesc(List.of(place.getId())); + + if (!images.isEmpty()) { + return images.get(0).imageUrl(); + } + return null; + } + + public Page getStoredTripRecords(PrincipalDetails principalDetails, Pageable pageable) { + Page storedTripRecords = tripRecordStoreRepository.findByMember(principalDetails.getMember(), pageable); + return storedTripRecords.map(tripRecordStore -> { + TripRecord tripRecord = tripRecordStore.getTripRecord(); + List images = tripRecord.getTripRecordImages(); + String imageUrl = images.isEmpty() ? null : images.get(0).getImageUrl(); + return TripRecordStoreResponseDto.fromEntity(tripRecordStore, imageUrl); + }); + } + + public Long getStoredCountForCity(Long cityId) { + City city = cityRepository.findById(cityId) + .orElseThrow(() -> new CityNotFoundException()); + + return cityStoreRepository.countByCity(city); + } + + public Long getStoredCountForPlace(Long placeId) { + Place place = placeRepository.findById(placeId) + .orElseThrow(() -> new PlaceNotFoundException()); + + return placeStoreRepository.countByPlace(place); + } + + public Long getStoredCountForTripRecord(Long tripRecordId) { + TripRecord tripRecord = tripRecordRepository.findById(tripRecordId) + .orElseThrow(() -> new TripRecordNotFoundException()); + + return tripRecordStoreRepository.countByTripRecord(tripRecord); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/controller/TripPlanController.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/controller/TripPlanController.java new file mode 100644 index 00000000..f400339c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/controller/TripPlanController.java @@ -0,0 +1,112 @@ +package com.haejwo.tripcometrue.domain.tripplan.controller; + +import com.haejwo.tripcometrue.domain.tripplan.dto.request.TripPlanRequestDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.CopyTripPlanFromTripRecordResponseDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.TripPlanDetailsResponseDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.TripPlanListReponseDto; +import com.haejwo.tripcometrue.domain.tripplan.sevice.TripPlanService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/trip-plan") +@RequiredArgsConstructor +public class TripPlanController { + + private final TripPlanService tripPlanService; + + @PostMapping + public ResponseEntity> createTripPlan( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody TripPlanRequestDto requestDto + ) { + + tripPlanService.addTripPlan(principalDetails, requestDto); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @DeleteMapping("/{planId}") + public ResponseEntity> deleteTripPlan( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long planId) { + + tripPlanService.deleteTripPlan(principalDetails, planId); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @PutMapping("/{planId}") + public ResponseEntity> modifyTripPlan( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody TripPlanRequestDto requestDto, + @PathVariable Long planId) { + + tripPlanService.modifyTripPlan(principalDetails, planId, requestDto); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/{planId}") + public ResponseEntity> getTripPlanDetails( + @PathVariable Long planId) { + + ResponseDTO responseBody = ResponseDTO.okWithData( + tripPlanService.getTripPlanDetails(planId)); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("from-trip-record/{tripRecordId}") + public ResponseEntity> copyTripPlanFromTripRecord( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId) { + + ResponseDTO responseBody = ResponseDTO.okWithData( + tripPlanService.copyTripPlanFromTripRecord(tripRecordId, principalDetails)); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/my") + public ResponseEntity>> getMyTripPlans( + @AuthenticationPrincipal PrincipalDetails principalDetails, + Pageable pageable + ) { + Page responseDtos + = tripPlanService.getMyTripPlansList(principalDetails, pageable); + ResponseDTO> responseBody + = ResponseDTO.okWithData(responseDtos); + + return ResponseEntity + .status(HttpStatus.OK) + .body(responseBody); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/controller/TripPlanControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/controller/TripPlanControllerAdvice.java new file mode 100644 index 00000000..20f744e4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/controller/TripPlanControllerAdvice.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.tripplan.controller; + +import com.haejwo.tripcometrue.domain.tripplan.exception.TripPlanNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class TripPlanControllerAdvice { + + @ExceptionHandler(TripPlanNotFoundException.class) + public ResponseEntity> TripPlanNotFoundException( + TripPlanNotFoundException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/request/TripPlanRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/request/TripPlanRequestDto.java new file mode 100644 index 00000000..4b6cfda9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/request/TripPlanRequestDto.java @@ -0,0 +1,33 @@ +package com.haejwo.tripcometrue.domain.tripplan.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; + +public record TripPlanRequestDto( + + @NotNull(message = "countries์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String countries, + @NotNull(message = "tripStartDay์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + LocalDate tripStartDay, + @NotNull(message = "tripEndDay์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + LocalDate tripEndDay, + Long referencedBy, + List tripPlanSchedules +) { + + public TripPlan toEntity(Member member) { + return TripPlan.builder() + .countries(this.countries) + .tripStartDay(this.tripStartDay) + .tripEndDay(this.tripEndDay) + .member(member) + .referencedBy(this.referencedBy) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/request/TripPlanScheduleRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/request/TripPlanScheduleRequestDto.java new file mode 100644 index 00000000..bea14a6e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/request/TripPlanScheduleRequestDto.java @@ -0,0 +1,32 @@ +package com.haejwo.tripcometrue.domain.tripplan.dto.request; + +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlanSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import jakarta.validation.constraints.NotNull; + +public record TripPlanScheduleRequestDto( + + @NotNull(message = "dayNumber์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + int dayNumber, + @NotNull(message = "orderNumber์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + int orderNumber, + @NotNull(message = "placeId์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + Long placeId, + String content, + ExternalLinkTagType tagType, + String tagUrl +) { + + public TripPlanSchedule toEntity(TripPlan tripPlan) { + return TripPlanSchedule.builder() + .dayNumber(this.dayNumber) + .ordering(this.orderNumber) + .content(this.content) + .placeId(this.placeId) + .tripPlan(tripPlan) + .tagType(this.tagType) + .tagUrl(this.tagUrl) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/CopyTripPlanFromTripRecordResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/CopyTripPlanFromTripRecordResponseDto.java new file mode 100644 index 00000000..a7e0e2d9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/CopyTripPlanFromTripRecordResponseDto.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.tripplan.dto.response; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import java.time.LocalDate; +import java.util.List; + +public record CopyTripPlanFromTripRecordResponseDto( + + String countries, + LocalDate tripStartDay, + LocalDate tripEndDay, + List tripSchedules +) { + + public static CopyTripPlanFromTripRecordResponseDto fromEntity(TripRecord tripRecord, + List tripSchedules) { + return new CopyTripPlanFromTripRecordResponseDto( + tripRecord.getCountries(), + tripRecord.getTripStartDay(), + tripRecord.getTripEndDay(), + tripSchedules + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanDetailsResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanDetailsResponseDto.java new file mode 100644 index 00000000..d9432633 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanDetailsResponseDto.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.tripplan.dto.response; + +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import java.time.LocalDate; +import java.util.List; + +public record TripPlanDetailsResponseDto( + + String countries, + LocalDate tripStartDay, + LocalDate tripEndDay, + List tripPlanSchedules +) { + + public static TripPlanDetailsResponseDto fromEntity(TripPlan tripPlan, + List tripPlanSchedules) { + return new TripPlanDetailsResponseDto( + tripPlan.getCountries(), + tripPlan.getTripStartDay(), + tripPlan.getTripEndDay(), + tripPlanSchedules + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanListReponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanListReponseDto.java new file mode 100644 index 00000000..bc5fb89b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanListReponseDto.java @@ -0,0 +1,36 @@ +package com.haejwo.tripcometrue.domain.tripplan.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public record TripPlanListReponseDto( + Long id, + String countries, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + LocalDate tripStartDay, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + LocalDate tripEndDay, + Integer totalDays, + Integer averageRating, + Integer viewCount, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt, + List placesVisited +) { + public static TripPlanListReponseDto fromEntity(TripPlan tripplan, List placesVisited) { + return new TripPlanListReponseDto( + tripplan.getId(), + tripplan.getCountries(), + tripplan.getTripStartDay(), + tripplan.getTripEndDay(), + tripplan.getTotalDays(), + tripplan.getAverageRating(), + tripplan.getViewCount(), + tripplan.getCreatedAt(), + placesVisited + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanScheduleResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanScheduleResponseDto.java new file mode 100644 index 00000000..4e4f6ed1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/dto/response/TripPlanScheduleResponseDto.java @@ -0,0 +1,58 @@ +package com.haejwo.tripcometrue.domain.tripplan.dto.response; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlanSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import com.haejwo.tripcometrue.global.enums.Country; + +public record TripPlanScheduleResponseDto( + + Double latitude, + Double longitude, + Country country, + String cityName, + String placeName, + Integer dayNumber, + Integer orderNumber, + Long placeId, + String content, + ExternalLinkTagType tagType, + String tagUrl + +) { + + public static TripPlanScheduleResponseDto fromEntity(TripPlanSchedule tripPlanSchedule, + Place place) { + return new TripPlanScheduleResponseDto( + place.getLatitude(), + place.getLongitude(), + place.getCity().getCountry(), + place.getCity().getName(), + place.getName(), + tripPlanSchedule.getDayNumber(), + tripPlanSchedule.getOrdering(), + place.getId(), + tripPlanSchedule.getContent(), + tripPlanSchedule.getTagType(), + tripPlanSchedule.getTagUrl() + ); + } + + public static TripPlanScheduleResponseDto fromEntity(TripRecordSchedule tripRecordSchedule, + Place place) { + return new TripPlanScheduleResponseDto( + place.getLatitude(), + place.getLongitude(), + place.getCity().getCountry(), + place.getCity().getName(), + place.getName(), + tripRecordSchedule.getDayNumber(), + tripRecordSchedule.getOrdering(), + place.getId(), + tripRecordSchedule.getContent(), + tripRecordSchedule.getTagType(), + tripRecordSchedule.getTagUrl() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/entity/TripPlan.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/entity/TripPlan.java new file mode 100644 index 00000000..c7d50d03 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/entity/TripPlan.java @@ -0,0 +1,70 @@ +package com.haejwo.tripcometrue.domain.tripplan.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.tripplan.dto.request.TripPlanRequestDto; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripPlan extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_plan_id") + private Long id; + + private String countries; + private LocalDate tripStartDay; + private LocalDate tripEndDay; + private Integer totalDays; + private Integer averageRating; + private Integer viewCount; + + @OneToMany(mappedBy = "tripPlan", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripPlanSchedules = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + private Long referencedBy; + + @Builder + public TripPlan(String countries, LocalDate tripStartDay, LocalDate tripEndDay, + Integer totalDays, Integer averageRating, Integer viewCount, + List tripRecordSchedules, Member member, Long referencedBy) { + this.countries = countries; + this.tripStartDay = tripStartDay; + this.tripEndDay = tripEndDay; + this.totalDays = totalDays; + this.averageRating = averageRating; + this.viewCount = 0; + this.tripPlanSchedules = tripRecordSchedules; + this.member = member; + this.referencedBy = referencedBy; + } + + public void update(TripPlanRequestDto requestDto) { + this.countries = requestDto.countries(); + this.tripStartDay = requestDto.tripStartDay(); + this.tripEndDay = requestDto.tripEndDay(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/entity/TripPlanSchedule.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/entity/TripPlanSchedule.java new file mode 100644 index 00000000..d573105f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/entity/TripPlanSchedule.java @@ -0,0 +1,58 @@ +package com.haejwo.tripcometrue.domain.tripplan.entity; + +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripPlanSchedule extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_plan_schedule_id") + private Long id; + + @Column(nullable = false) + private Integer dayNumber; + + @Column(nullable = false) + private Integer ordering; + + private String content; + private Long placeId; + + @ManyToOne + @JoinColumn(name = "trip_plan_id") + private TripPlan tripPlan; + + @Enumerated(EnumType.STRING) + private ExternalLinkTagType tagType; + private String tagUrl; + + @Builder + public TripPlanSchedule(Integer dayNumber, Integer ordering, String content, + Long placeId, TripPlan tripPlan, ExternalLinkTagType tagType, String tagUrl + ) { + this.dayNumber = dayNumber; + this.ordering = ordering; + this.content = content; + this.placeId = placeId; + this.tripPlan = tripPlan; + this.tagType = tagType; + this.tagUrl = tagUrl; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/exception/TripPlanNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/exception/TripPlanNotFoundException.java new file mode 100644 index 00000000..384ec62f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/exception/TripPlanNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.tripplan.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripPlanNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_PLAN_NOT_FOUND; + + public TripPlanNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/repository/TripPlanRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/repository/TripPlanRepository.java new file mode 100644 index 00000000..8bcc883e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/repository/TripPlanRepository.java @@ -0,0 +1,19 @@ +package com.haejwo.tripcometrue.domain.tripplan.repository; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface TripPlanRepository extends JpaRepository { + + Page findByMemberId(Long memberId, Pageable pageable); + + @Query("select tp from TripPlan tp where tp.member = :member and tp.referencedBy = :tripRecordId order by tp.tripEndDay asc limit 1") + Optional findByMemberIdAndTripRecordId(@Param("member") Member loginMember, @Param("tripRecordId") Long tripRecordId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/repository/TripPlanScheduleRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/repository/TripPlanScheduleRepository.java new file mode 100644 index 00000000..df848027 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/repository/TripPlanScheduleRepository.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.tripplan.repository; + +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlanSchedule; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripPlanScheduleRepository extends JpaRepository { + + void deleteAllByTripPlanId(Long tripPlanId); + + List findAllByTripPlanId(Long tripPlanId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/tripplan/sevice/TripPlanService.java b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/sevice/TripPlanService.java new file mode 100644 index 00000000..2b602dad --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/tripplan/sevice/TripPlanService.java @@ -0,0 +1,155 @@ +package com.haejwo.tripcometrue.domain.tripplan.sevice; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.exception.PlaceNotFoundException; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.store.entity.TripRecordStore; +import com.haejwo.tripcometrue.domain.tripplan.dto.request.TripPlanRequestDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.request.TripPlanScheduleRequestDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.CopyTripPlanFromTripRecordResponseDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.TripPlanDetailsResponseDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.TripPlanListReponseDto; +import com.haejwo.tripcometrue.domain.tripplan.dto.response.TripPlanScheduleResponseDto; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlan; +import com.haejwo.tripcometrue.domain.tripplan.entity.TripPlanSchedule; +import com.haejwo.tripcometrue.domain.tripplan.exception.TripPlanNotFoundException; +import com.haejwo.tripcometrue.domain.tripplan.repository.TripPlanRepository; +import com.haejwo.tripcometrue.domain.tripplan.repository.TripPlanScheduleRepository; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule.TripRecordScheduleRepository; +import com.haejwo.tripcometrue.global.exception.PermissionDeniedException; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TripPlanService { + + private final TripPlanRepository tripPlanRepository; + private final TripPlanScheduleRepository tripPlanScheduleRepository; + private final PlaceRepository placeRepository; + private final TripRecordRepository tripRecordRepository; + private final TripRecordScheduleRepository tripRecordScheduleRepository; + + @Transactional + public void addTripPlan(PrincipalDetails principalDetails, TripPlanRequestDto requestDto) { + validatePlaceId(requestDto.tripPlanSchedules()); + TripPlan requestTripPlan = requestDto.toEntity(principalDetails.getMember()); + tripPlanRepository.save(requestTripPlan); + + requestDto.tripPlanSchedules().stream() + .map(tripPlanScheduleRequestDto -> + tripPlanScheduleRequestDto.toEntity(requestTripPlan)) + .forEach(tripPlanScheduleRepository::save); + } + + public void deleteTripPlan(PrincipalDetails principalDetails, Long planId) { + + TripPlan tripPlan = tripPlanRepository.findById(planId) + .orElseThrow(TripPlanNotFoundException::new); + + if (!tripPlan.getMember().getId().equals(principalDetails.getMember().getId())) { + throw new PermissionDeniedException(); + } + + tripPlanRepository.delete(tripPlan); + } + + @Transactional + public void modifyTripPlan(PrincipalDetails principalDetails, Long planId, + TripPlanRequestDto requestDto) { + + validatePlaceId(requestDto.tripPlanSchedules()); + TripPlan tripPlan = tripPlanRepository.findById(planId) + .orElseThrow(TripPlanNotFoundException::new); + + if (!tripPlan.getMember().getId().equals(principalDetails.getMember().getId())) { + throw new PermissionDeniedException(); + } + + tripPlan.update(requestDto); + tripPlanRepository.save(tripPlan); + + tripPlanScheduleRepository.deleteAllByTripPlanId(tripPlan.getId()); + requestDto.tripPlanSchedules().stream() + .map(tripPlanScheduleRequestDto -> + tripPlanScheduleRequestDto.toEntity(tripPlan)) + .forEach(tripPlanScheduleRepository::save); + } + + public TripPlanDetailsResponseDto getTripPlanDetails(Long planId) { + + TripPlan tripPlan = tripPlanRepository.findById(planId) + .orElseThrow(TripPlanNotFoundException::new); + + List responseDtos = tripPlanScheduleRepository + .findAllByTripPlanId(tripPlan.getId()) + .stream() + .map(tripPlanSchedule -> { + Place place = placeRepository.findById(tripPlanSchedule.getPlaceId()) + .orElseThrow(PlaceNotFoundException::new); + return TripPlanScheduleResponseDto.fromEntity(tripPlanSchedule, place); + }) + .collect(Collectors.toList()); + + return TripPlanDetailsResponseDto.fromEntity(tripPlan, responseDtos); + } + + private void validatePlaceId(List tripPlanSchedules) { + for (TripPlanScheduleRequestDto tripPlanSchedule : tripPlanSchedules) { + placeRepository.findById(tripPlanSchedule.placeId()) + .orElseThrow(PlaceNotFoundException::new); + } + } + + public CopyTripPlanFromTripRecordResponseDto copyTripPlanFromTripRecord( + Long tripRecordId, + PrincipalDetails principalDetails) { + + TripRecordStore tripRecordStore = TripRecordStore + .builder() + .member(principalDetails.getMember()) + .tripRecord(tripRecordRepository.findById(tripRecordId) + .orElseThrow(TripRecordNotFoundException::new)) + .build(); + + List responseDtos = tripRecordScheduleRepository + .findAllByTripRecordId(tripRecordId) + .stream() + .map(tripRecordSchedule -> { + Place place = placeRepository.findById(tripRecordSchedule.getPlace().getId()) + .orElseThrow(PlaceNotFoundException::new); + return TripPlanScheduleResponseDto.fromEntity(tripRecordSchedule, place); + }) + .toList(); + + return CopyTripPlanFromTripRecordResponseDto.fromEntity( + tripRecordStore.getTripRecord(), + responseDtos); + } + + @Transactional(readOnly = true) + public Page getMyTripPlansList( + PrincipalDetails principalDetails, Pageable pageable) { + Long memberId = principalDetails.getMember().getId(); + Page tripPlans = tripPlanRepository.findByMemberId(memberId, pageable); + + return tripPlans.map(tripPlan -> { + List placesVisited = tripPlan.getTripPlanSchedules().stream() + .map(TripPlanSchedule::getPlaceId) + .map(placeId -> placeRepository.findById(placeId) + .map(Place::getName) + .orElse(null)) // ์žฅ์†Œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ null ๋ฐ˜ํ™˜ + .collect(Collectors.toList()); + + return TripPlanListReponseDto.fromEntity(tripPlan, placesVisited); + }); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordController.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordController.java new file mode 100644 index 00000000..1d329253 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordController.java @@ -0,0 +1,182 @@ +package com.haejwo.tripcometrue.domain.triprecord.controller; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.MyTripRecordListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordSearchParamAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordDetailResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordHotShortsListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoDetailDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.domain.triprecord.service.TripRecordService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import com.haejwo.tripcometrue.global.validator.annotation.HomeTopListQueryType; +import java.util.List; + +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/v1/trip-records") +@RequiredArgsConstructor +public class TripRecordController { + + private final TripRecordService tripRecordService; + + @GetMapping("/{tripRecordId}") + public ResponseEntity> tripRecordDetail( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId + ) { + + TripRecordDetailResponseDto responseDto = tripRecordService.findTripRecord(principalDetails, + tripRecordId); + ResponseDTO responseBody = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping + public ResponseEntity>> tripRecordList( + Pageable pageable, + @ModelAttribute TripRecordListRequestAttribute request + ) { + + List responseDtos + = tripRecordService.findTripRecordList(pageable, request); + + ResponseDTO> responseBody = ResponseDTO.okWithData( + responseDtos); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + + } + + // ์—ฌํ–‰ ํ›„๊ธฐ ๊ฒ€์ƒ‰(์ด ์ผ์ˆ˜, ๋„์‹œ๋ช…, ์—ฌํ–‰์ง€๋ช…, ๋„์‹œ ์‹๋ณ„์ž) ๋ฐ ํŽ˜์ด์ง• ์กฐํšŒ + @GetMapping("/search") + public ResponseEntity>> searchTripRecords( + @ModelAttribute TripRecordSearchParamAttribute searchParamAttribute, + @PageableDefault(sort = "averageRating", direction = Sort.Direction.DESC) Pageable pageable + ) { + + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + tripRecordService.findTripRecordList(searchParamAttribute, pageable) + ) + ); + } + + // ์—ฌํ–‰ ํ›„๊ธฐ ๊ฒ€์ƒ‰(ํ•ด์‹œํƒœ๊ทธ) ๋ฐ ํŽ˜์ด์ง• ์กฐํšŒ + @GetMapping("/search/hashtags") + public ResponseEntity>> searchTripRecordsByHashtag( + @RequestParam("hashtag") String hashtag, + @PageableDefault(sort = "averageRating", direction = Sort.Direction.DESC) Pageable pageable + ) { + + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + tripRecordService.listTripRecordsByHashtag(hashtag, pageable) + ) + ); + } + + // ์—ฌํ–‰ ํ›„๊ธฐ ๊ฒ€์ƒ‰(์—ฌํ–‰ ๊ฒฝ๋น„ ๋ฒ”์œ„) ๋ฐ ํŽ˜์ด์ง• ์กฐํšŒ + @GetMapping("/search/expense-ranges") + public ResponseEntity>> searchTripRecordsByExpenseRangeType( + @RequestParam("expenseRangeType") + @NotNull(message = "์š”์ฒญ๊ฐ’์€ [BELOW_50, BELOW_100, BELOW_200, BELOW_300, ABOVE_300] ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + ExpenseRangeType expenseRangeType, + @PageableDefault(sort = "averageRating", direction = Sort.Direction.DESC) Pageable pageable + ) { + + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + tripRecordService.listTripRecordsByExpenseRangeType(expenseRangeType, pageable) + ) + ); + } + + // ํ™ˆ ํ”ผ๋“œ TOP ์ธ๊ธฐ ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @GetMapping("/top-list") + public ResponseEntity>> listTopTripRecords( + @RequestParam("type") @HomeTopListQueryType String type + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + tripRecordService.findTopTripRecordList(type) + ) + ); + } + + @GetMapping("/my") + public ResponseEntity>> getMyTripRecords( + @AuthenticationPrincipal PrincipalDetails principalDetails, + Pageable pageable + ) { + Page responseDtos + = tripRecordService.getMyTripRecordsList(principalDetails, pageable); + ResponseDTO> responseBody + = ResponseDTO.okWithData(responseDtos); + + return ResponseEntity + .status(HttpStatus.OK) + .body(responseBody); + + } + + @GetMapping("hot-shorts-list") + public ResponseEntity>> tripRecordHotShortsList( + Pageable pageable + ) { + List responseDtos = tripRecordService.findTripRecordHotShortsList(pageable); + + ResponseDTO> responseBody = ResponseDTO.okWithData(responseDtos); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("shorts/{videoId}") + public ResponseEntity> tripRecordShortsDetail( + @PathVariable Long videoId + ) { + + TripRecordScheduleVideoDetailDto responseDto = tripRecordService.findTripRecordShortsDetail(videoId); + + ResponseDTO responseBody = ResponseDTO.okWithData(responseDto); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordControllerAdvice.java new file mode 100644 index 00000000..e7025ee2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordControllerAdvice.java @@ -0,0 +1,48 @@ +package com.haejwo.tripcometrue.domain.triprecord.controller; + +import com.haejwo.tripcometrue.domain.triprecord.exception.ExpenseRangeTypeNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordScheduleVideoNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class TripRecordControllerAdvice { + + @ExceptionHandler(TripRecordNotFoundException.class) + public ResponseEntity> tripRecordNotFoundException( + TripRecordNotFoundException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(ExpenseRangeTypeNotFoundException.class) + public ResponseEntity> expenseRangeTypeNotFoundException( + ExpenseRangeTypeNotFoundException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(TripRecordScheduleVideoNotFoundException.class) + public ResponseEntity> tripRecordScheduleVideoNotFoundException( + TripRecordScheduleVideoNotFoundException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordEditController.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordEditController.java new file mode 100644 index 00000000..1cf53e94 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordEditController.java @@ -0,0 +1,112 @@ +package com.haejwo.tripcometrue.domain.triprecord.controller; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.CreateSchedulePlaceRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.TripRecordRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.GetCountryResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule.SearchScheduleTripResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.service.TripRecordEditService; +import com.haejwo.tripcometrue.global.enums.Continent; +import com.haejwo.tripcometrue.global.enums.Country; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class TripRecordEditController { + + private final TripRecordEditService tripRecordEditService; + + @PostMapping("/v1/trip-record") + public ResponseEntity> createTripRecord( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody TripRecordRequestDto requestDto + ) { + + tripRecordEditService.addTripRecord(principalDetails, requestDto); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @PutMapping("/v1/trip-record/{tripRecordId}") + public ResponseEntity> modifyTripRecord( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @RequestBody TripRecordRequestDto requestDto, + @PathVariable Long tripRecordId + ) { + + tripRecordEditService.modifyTripRecord(principalDetails, requestDto, tripRecordId); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @DeleteMapping("/v1/trip-record/{tripRecordId}") + public ResponseEntity> deleteTripRecord( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PathVariable Long tripRecordId + ) { + + tripRecordEditService.deleteTripRecord(principalDetails, tripRecordId); + ResponseDTO responseBody = ResponseDTO.ok(); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/v1/search-schedule-places") + public ResponseEntity>> searchSchedulePlace( + @RequestParam Country country, + @RequestParam String city + ) { + ResponseDTO> responseBody + = ResponseDTO.okWithData(tripRecordEditService.searchSchedulePlace(country, city)); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @PostMapping("/v1/schedule-place") + public ResponseEntity> createSchedulePlace( + @RequestBody CreateSchedulePlaceRequestDto createSchedulePlaceRequestDto + ) { + ResponseDTO responseBody + = ResponseDTO.okWithData( + tripRecordEditService.createSchedulePlace(createSchedulePlaceRequestDto)); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + + @GetMapping("/v1/country-city") + public ResponseEntity>> getCountryCity( + @RequestParam(required = false) Continent continent + ) { + ResponseDTO> responseBody + = ResponseDTO.okWithData( + tripRecordEditService.getCountryCity(continent)); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordEditControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordEditControllerAdvice.java new file mode 100644 index 00000000..f7332736 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordEditControllerAdvice.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.triprecord.controller; + +import com.haejwo.tripcometrue.global.exception.PermissionDeniedException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class TripRecordEditControllerAdvice { + + @ExceptionHandler(PermissionDeniedException.class) + public ResponseEntity> PermissionDeniedException( + PermissionDeniedException e + ) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordScheduleController.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordScheduleController.java new file mode 100644 index 00000000..4e94fce7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordScheduleController.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.domain.triprecord.controller; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordScheduleImageListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleImageListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.service.TripRecordScheduleService; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/v1/trip-records-schedules") +public class TripRecordScheduleController { + + private final TripRecordScheduleService tripRecordScheduleService; + + @GetMapping + public ResponseEntity>> tripRecordScheduleImageList( + Pageable pageable, + @ModelAttribute TripRecordScheduleImageListRequestAttribute requestParam + ) { + + Page responseDtos = tripRecordScheduleService.findScheduleImages(pageable, requestParam); + + ResponseDTO> responseBody = ResponseDTO.okWithData(responseDtos); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordScheduleVideoController.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordScheduleVideoController.java new file mode 100644 index 00000000..13ba9719 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/controller/TripRecordScheduleVideoController.java @@ -0,0 +1,35 @@ +package com.haejwo.tripcometrue.domain.triprecord.controller; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.service.TripRecordScheduleVideoService; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import com.haejwo.tripcometrue.global.validator.annotation.HomeVideoListQueryType; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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 java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/v1/videos") +@RestController +public class TripRecordScheduleVideoController { + + private final TripRecordScheduleVideoService tripRecordScheduleVideoService; + + @GetMapping("/list") + public ResponseEntity>> listVideosByRequestType( + @RequestParam("type") @HomeVideoListQueryType String type + ) { + return ResponseEntity + .ok() + .body( + ResponseDTO.okWithData( + tripRecordScheduleVideoService.getNewestVideos(type) + ) + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordScheduleImageWithPlaceIdQueryDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordScheduleImageWithPlaceIdQueryDto.java new file mode 100644 index 00000000..72949c83 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordScheduleImageWithPlaceIdQueryDto.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.query; + +import java.time.LocalDateTime; + +public record TripRecordScheduleImageWithPlaceIdQueryDto( + Long placeId, + String imageUrl, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordScheduleVideoQueryDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordScheduleVideoQueryDto.java new file mode 100644 index 00000000..3c8b7471 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordScheduleVideoQueryDto.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.query; + +public record TripRecordScheduleVideoQueryDto( + Long id, + Long tripRecordId, + String tripRecordTitle, + String thumbnailUrl, + String videoUrl, + Integer storedCount, + Long memberId, + String memberName, + String profileImageUrl +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordViewHistoryGroupByQueryDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordViewHistoryGroupByQueryDto.java new file mode 100644 index 00000000..88911829 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/query/TripRecordViewHistoryGroupByQueryDto.java @@ -0,0 +1,10 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.query; + +public record TripRecordViewHistoryGroupByQueryDto( + Long memberId, + String nickname, + String introduction, + String profileImageUrl, + Long totalCount +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/CreateSchedulePlaceRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/CreateSchedulePlaceRequestDto.java new file mode 100644 index 00000000..a2461b58 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/CreateSchedulePlaceRequestDto.java @@ -0,0 +1,33 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.global.enums.Country; +import jakarta.validation.constraints.NotNull; + +public record CreateSchedulePlaceRequestDto( + + @NotNull(message = "address์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String address, + @NotNull(message = "name์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String name, + @NotNull(message = "latitude์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + Double latitude, + @NotNull(message = "longitude์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + Double longitude, + @NotNull(message = "country์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + Country country, + @NotNull(message = "cityname์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String cityname +) { + + public Place toEntity(City city) { + return Place.builder() + .name(this.name) + .address(this.address) + .latitude(this.latitude) + .longitude(this.longitude) + .city(city) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordListRequestAttribute.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordListRequestAttribute.java new file mode 100644 index 00000000..eca0898d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordListRequestAttribute.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute; + +import com.querydsl.core.types.Order; + +public record TripRecordListRequestAttribute( + String hashtag, + Integer expenseRangeType, + Integer totalDays, + Long placeId, + String orderBy, + Order order +) { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordScheduleImageListRequestAttribute.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordScheduleImageListRequestAttribute.java new file mode 100644 index 00000000..9ed94085 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordScheduleImageListRequestAttribute.java @@ -0,0 +1,8 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute; + +public record TripRecordScheduleImageListRequestAttribute( + Long placeId, + String orderBy +) { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordSearchParamAttribute.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordSearchParamAttribute.java new file mode 100644 index 00000000..5d33b254 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/ModelAttribute/TripRecordSearchParamAttribute.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute; + +public record TripRecordSearchParamAttribute( + String cityName, + String placeName, + Long cityId, + String totalDays +) { +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordImageRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordImageRequestDto.java new file mode 100644 index 00000000..2f111fe2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordImageRequestDto.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; + +public record TripRecordImageRequestDto( + + String imageUrl, + ExternalLinkTagType tagType, + String tagUrl +) { + + public TripRecordImage toEntity(TripRecord tripRecord) { + return TripRecordImage.builder() + .imageUrl(this.imageUrl) + .tagType(this.tagType) + .tagUrl(this.tagUrl) + .tripRecord(tripRecord) + .build(); + + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordRequestDto.java new file mode 100644 index 00000000..aa5ae43e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordRequestDto.java @@ -0,0 +1,49 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.List; + +public record TripRecordRequestDto( + + List tripRecordImages, + @NotNull(message = "title์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String title, + @NotNull(message = "content์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String content, + ExpenseRangeType expenseRangeType, + List hashTags, + @NotNull(message = "countries์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + String countries, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripStartDay, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripEndDay, + List tripRecordSchedules +) { + + public TripRecord toEntity() { + return TripRecord.builder() + .title(this.title) + .content(this.content) + .expenseRangeType(this.expenseRangeType) + .tripStartDay(this.tripStartDay) + .tripEndDay(this.tripEndDay) + .countries(this.countries) + .build(); + } + + public TripRecord toEntity(Member member) { + return TripRecord.builder() + .title(this.title) + .content(this.content) + .expenseRangeType(this.expenseRangeType) + .tripStartDay(this.tripStartDay) + .tripEndDay(this.tripEndDay) + .countries(this.countries) + .member(member) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordScheduleRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordScheduleRequestDto.java new file mode 100644 index 00000000..4cff96ed --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/request/TripRecordScheduleRequestDto.java @@ -0,0 +1,36 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.request; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record TripRecordScheduleRequestDto( + + @NotNull(message = "dayNumber์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + int dayNumber, + @NotNull(message = "orderNumber์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + int orderNumber, + @NotNull(message = "placeId์€ ํ•„์ˆ˜๊ฐ’์ž…๋‹ˆ๋‹ค") + Long placeId, + String content, + List tripRecordScheduleImages, + List tripRecordScheduleVideos, + ExternalLinkTagType tagType, + String tagUrl +) { + + public TripRecordSchedule toEntity(TripRecord tripRecord, Place place) { + return TripRecordSchedule.builder() + .dayNumber(this.dayNumber) + .ordering(this.orderNumber) + .content(this.content) + .place(place) + .tripRecord(tripRecord) + .tagType(this.tagType) + .tagUrl(this.tagUrl) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/member/TripRecordMemberResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/member/TripRecordMemberResponseDto.java new file mode 100644 index 00000000..ae30b127 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/member/TripRecordMemberResponseDto.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.member; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import lombok.Builder; + +public record TripRecordMemberResponseDto( + String nickname, + String profileImage +) { + + @Builder + public TripRecordMemberResponseDto(String nickname, String profileImage) { + this.nickname = nickname; + this.profileImage = profileImage; + } + + public static TripRecordMemberResponseDto fromEntity(Member entity) { + return TripRecordMemberResponseDto.builder() + .nickname(entity.getMemberBase().getNickname()) + .profileImage(entity.getProfileImage()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/member/TripRecordVideoMemberResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/member/TripRecordVideoMemberResponseDto.java new file mode 100644 index 00000000..5fd26e5c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/member/TripRecordVideoMemberResponseDto.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.member; + +import lombok.Builder; + +public record TripRecordVideoMemberResponseDto( + Long memberId, + String nickname, + String profileImage +) { + + @Builder + public TripRecordVideoMemberResponseDto(Long memberId, String nickname, String profileImage) { + this.memberId = memberId; + this.nickname = nickname; + this.profileImage = profileImage; + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/GetCountryCityResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/GetCountryCityResponseDto.java new file mode 100644 index 00000000..162bdb61 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/GetCountryCityResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import lombok.Builder; + +public record GetCountryCityResponseDto( + Long cityId, + String cityName, + String cityImageUrl + +) { + + @Builder + public GetCountryCityResponseDto(Long cityId, String cityName, String cityImageUrl) { + this.cityId = cityId; + this.cityName = cityName; + this.cityImageUrl = cityImageUrl; + } + + public static GetCountryCityResponseDto fromEntity(City city) { + return GetCountryCityResponseDto.builder() + .cityId(city.getId()) + .cityName(city.getName()) + .cityImageUrl(city.getImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/GetCountryResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/GetCountryResponseDto.java new file mode 100644 index 00000000..7ae1f69a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/GetCountryResponseDto.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.haejwo.tripcometrue.global.enums.Continent; +import com.haejwo.tripcometrue.global.enums.Country; +import java.util.List; + +public record GetCountryResponseDto( + Continent continent, + Country country, + String countryName, + String countryImageUrl, + List cityList +) { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/MyTripRecordListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/MyTripRecordListResponseDto.java new file mode 100644 index 00000000..1272ae4c --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/MyTripRecordListResponseDto.java @@ -0,0 +1,50 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordMemberResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import java.util.List; +import lombok.Builder; + +public record MyTripRecordListResponseDto( + Long tripRecordId, + String title, + String countries, + Integer totalDays, + Integer commentCount, + Integer storeCount, + String imageUrl, + TripRecordMemberResponseDto member +) { + + @Builder + public MyTripRecordListResponseDto(Long tripRecordId, String title, String countries, Integer totalDays, + Integer commentCount, Integer storeCount, String imageUrl, + TripRecordMemberResponseDto member) { + this.tripRecordId = tripRecordId; + this.title = title; + this.countries = countries; + this.totalDays = totalDays; + this.commentCount = commentCount; + this.storeCount = storeCount; + this.imageUrl = imageUrl; + this.member = member; + } + + + public static MyTripRecordListResponseDto fromEntity(TripRecord entity) { + List images = entity.getTripRecordImages(); + String imageUrl = images.isEmpty() ? null : images.get(0).getImageUrl(); + + return MyTripRecordListResponseDto.builder() + .tripRecordId(entity.getId()) + .title(entity.getTitle()) + .countries(entity.getCountries()) + .totalDays(entity.getTotalDays()) + .commentCount(entity.getCommentCount()) + .storeCount(entity.getStoreCount()) + .imageUrl(imageUrl) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordDetailResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordDetailResponseDto.java new file mode 100644 index 00000000..e2fb065b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordDetailResponseDto.java @@ -0,0 +1,123 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordMemberResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_media_tag.TripRecordImageResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_media_tag.TripRecordTagResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule.TripRecordScheduleDetailResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordTag; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Builder; + +public record TripRecordDetailResponseDto( + Long id, + String title, + String content, + ExpenseRangeType expenseRangeType, + String countries, + @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripStartDay, + @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripEndDay, + Integer totalDays, + Double averageRating, + Integer storeCount, + Boolean isStored, + TripRecordMemberResponseDto member, + List images, + List tags, + Map> schedules + +) { + + @Builder + public TripRecordDetailResponseDto(Long id, String title, String content, + ExpenseRangeType expenseRangeType, String countries, + @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripStartDay, + @JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripEndDay, + Integer totalDays, Double averageRating, Integer storeCount, Boolean isStored, + TripRecordMemberResponseDto member, List images, + List tags, + Map> schedules) { + this.id = id; + this.title = title; + this.content = content; + this.expenseRangeType = expenseRangeType; + this.countries = countries; + this.tripStartDay = tripStartDay; + this.tripEndDay = tripEndDay; + this.totalDays = totalDays; + this.averageRating = averageRating; + this.storeCount = storeCount; + this.isStored = isStored; + this.member = member; + this.images = images; + this.tags = tags; + this.schedules = schedules; + } + + public static TripRecordDetailResponseDto fromEntity(TripRecord entity, Boolean isStored) { + + TripRecordMemberResponseDto member = TripRecordMemberResponseDto.fromEntity(entity.getMember()); + + List images = entity.getTripRecordImages(); + List imageDtos = images.stream() + .map(TripRecordImageResponseDto::fromEntity) + .toList(); + + List tags = entity.getTripRecordTags(); + List tagDtos = tags.stream() + .map(TripRecordTagResponseDto::fromEntity) + .toList(); + + List schedules = entity.getTripRecordSchedules(); + Map> scheduleDtos = schedules.stream() + .map(TripRecordScheduleDetailResponseDto::fromEntity) + .sorted(Comparator.comparing(TripRecordScheduleDetailResponseDto::ordering)) + .collect(Collectors.groupingBy( + dto -> entity.getTripStartDay().plusDays(dto.dayNumber() - 1), + LinkedHashMap::new, + Collectors.toList() + )); + // List ํƒ€์ž…์œผ๋กœ ์‘๋‹ตํ•  ์‹œ +// List> scheduleDtos = new ArrayList<>( +// schedules.stream() +// .map(TripRecordScheduleDetailResponseDto::fromEntity) +// .collect(Collectors.groupingBy( +// TripRecordScheduleDetailResponseDto::dayNumber, +// LinkedHashMap::new, +// Collectors.collectingAndThen( +// Collectors.toList(), +// list -> list.stream().sorted(Comparator.comparing(TripRecordScheduleDetailResponseDto::ordering)).collect(Collectors.toList()) +// ) +// )).values() +// ); + + return TripRecordDetailResponseDto.builder() + .id(entity.getId()) + .title(entity.getTitle()) + .content(entity.getContent()) + .expenseRangeType(entity.getExpenseRangeType()) + .countries(entity.getCountries()) + .tripStartDay(entity.getTripStartDay()) + .tripEndDay(entity.getTripEndDay()) + .totalDays(entity.getTotalDays()) + .storeCount(entity.getStoreCount()) + .isStored(isStored) + .averageRating(entity.getAverageRating()) + .member(member) + .images(imageDtos) + .tags(tagDtos) + .schedules(scheduleDtos) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordListItemResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordListItemResponseDto.java new file mode 100644 index 00000000..fd6c99c3 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordListItemResponseDto.java @@ -0,0 +1,58 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import lombok.Builder; + +import java.util.Objects; +import java.util.Set; + +public record TripRecordListItemResponseDto( + Long tripRecordId, + String tripRecordTitle, + Long memberId, + String memberName, + String profileImageUrl, + Set cityNames, + Integer totalDays, + Double averageRating, + Integer storedCount, + String imageUrl +) { + + @Builder + public TripRecordListItemResponseDto { + } + + public static TripRecordListItemResponseDto fromEntity( + TripRecord entity, Set cityNames, Member member + ) { + boolean isPresentMember = Objects.nonNull(member); + return TripRecordListItemResponseDto.builder() + .tripRecordId(entity.getId()) + .tripRecordTitle(entity.getTitle()) + .memberId( + isPresentMember ? member.getId() : null + ) + .memberName( + isPresentMember ? member.getMemberBase().getNickname() : null + ) + .profileImageUrl( + isPresentMember ? member.getProfileImage() : null + ) + .cityNames(cityNames) + .totalDays(entity.getTotalDays()) + .averageRating(entity.getAverageRating()) + .storedCount(entity.getStoreCount()) + .imageUrl( + entity.getTripRecordImages() + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(TripRecordImage::getImageUrl) + .orElse(null) + ) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordListResponseDto.java new file mode 100644 index 00000000..c3b0ec32 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordListResponseDto.java @@ -0,0 +1,44 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordMemberResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import lombok.Builder; + +public record TripRecordListResponseDto( + Long tripRecordId, + String title, + String countries, + Integer totalDays, + Integer commentCount, + Integer storeCount, + Double averageRating, + String imageUrl, + TripRecordMemberResponseDto member +) { + + @Builder + public TripRecordListResponseDto(Long tripRecordId, String title, String countries, + Integer totalDays, Integer commentCount, Integer storeCount, Double averageRating, + String imageUrl, TripRecordMemberResponseDto member) { + this.tripRecordId = tripRecordId; + this.title = title; + this.countries = countries; + this.totalDays = totalDays; + this.commentCount = commentCount; + this.storeCount = storeCount; + this.averageRating = averageRating; + this.imageUrl = imageUrl; + this.member = member; + } + + + public static TripRecordListResponseDto fromEntity(TripRecord entity) { + return TripRecordListResponseDto.builder() + .tripRecordId(entity.getId()) + .title(entity.getTitle()) + .countries(entity.getCountries()) + .totalDays(entity.getTotalDays()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordResponseDto.java new file mode 100644 index 00000000..ca901b02 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord/TripRecordResponseDto.java @@ -0,0 +1,57 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import java.time.LocalDate; +import lombok.Builder; + +public record TripRecordResponseDto( + Long id, + String title, + String content, + ExpenseRangeType expenseRangeType, + String countries, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripStartDay, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate tripEndDay, + Integer totalDays, + Integer viewCount, + Double averageRating, + Long memberId + +) { + + @Builder + public TripRecordResponseDto(Long id, String title, String content, + ExpenseRangeType expenseRangeType, String countries, LocalDate tripStartDay, LocalDate tripEndDay, + Integer totalDays, Integer viewCount, Double averageRating, Long memberId) { + this.id = id; + this.title = title; + this.content = content; + this.expenseRangeType = expenseRangeType; + this.countries = countries; + this.tripStartDay = tripStartDay; + this.tripEndDay = tripEndDay; + this.totalDays = totalDays; + this.viewCount = viewCount; + this.averageRating = averageRating; + this.memberId = memberId; + } + + public static TripRecordResponseDto fromEntity(TripRecord entity) { + return TripRecordResponseDto.builder() + .id(entity.getId()) + .title(entity.getTitle()) + .content(entity.getContent()) + .expenseRangeType(entity.getExpenseRangeType()) + .countries(entity.getCountries()) + .tripStartDay(entity.getTripStartDay()) + .tripEndDay(entity.getTripEndDay()) + .totalDays(entity.getTotalDays()) + .viewCount(entity.getViewCount()) + .averageRating(entity.getAverageRating()) + .memberId(entity.getMember().getId()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_media_tag/TripRecordImageResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_media_tag/TripRecordImageResponseDto.java new file mode 100644 index 00000000..a5c2ff67 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_media_tag/TripRecordImageResponseDto.java @@ -0,0 +1,36 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_media_tag; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import lombok.Builder; + +public record TripRecordImageResponseDto( + Long id, + String imageUrl, + ExternalLinkTagType tagType, + String tagUrl, + Long tripRecordId + +) { + + @Builder + public TripRecordImageResponseDto(Long id, String imageUrl, ExternalLinkTagType tagType, + String tagUrl, Long tripRecordId) { + this.id = id; + this.imageUrl = imageUrl; + this.tagType = tagType; + this.tagUrl = tagUrl; + this.tripRecordId = tripRecordId; + } + + public static TripRecordImageResponseDto fromEntity(TripRecordImage entity) { + return TripRecordImageResponseDto.builder() + .id(entity.getId()) + .imageUrl(entity.getImageUrl()) + .tagType(entity.getTagType()) + .tagUrl(entity.getTagUrl()) + .tripRecordId(entity.getTripRecord().getId()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_media_tag/TripRecordTagResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_media_tag/TripRecordTagResponseDto.java new file mode 100644 index 00000000..bd4172cc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_media_tag/TripRecordTagResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_media_tag; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordTag; +import lombok.Builder; + +public record TripRecordTagResponseDto( + Long id, + String hashTagType, + Long tripRecordId +) { + + @Builder + public TripRecordTagResponseDto(Long id, String hashTagType, Long tripRecordId) { + this.id = id; + this.hashTagType = hashTagType; + this.tripRecordId = tripRecordId; + } + + public static TripRecordTagResponseDto fromEntity(TripRecordTag entity) { + return TripRecordTagResponseDto.builder() + .id(entity.getId()) + .hashTagType(entity.getHashTagType()) + .tripRecordId(entity.getTripRecord().getId()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule/SearchScheduleTripResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule/SearchScheduleTripResponseDto.java new file mode 100644 index 00000000..cd0ac526 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule/SearchScheduleTripResponseDto.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule; + +import com.haejwo.tripcometrue.domain.place.entity.Place; + +public record SearchScheduleTripResponseDto( + Long placeId, + String address, + String name +) { + + public static SearchScheduleTripResponseDto fromEntity(Place entity) { + return new SearchScheduleTripResponseDto( + entity.getId(), + entity.getAddress(), + entity.getName() + ); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule/TripRecordScheduleDetailResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule/TripRecordScheduleDetailResponseDto.java new file mode 100644 index 00000000..093a65cc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule/TripRecordScheduleDetailResponseDto.java @@ -0,0 +1,87 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleImageResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import java.util.List; +import lombok.Builder; + +public record TripRecordScheduleDetailResponseDto( + Long id, + Integer dayNumber, + Integer ordering, + String content, + ExternalLinkTagType tagType, + String tagUrl, + String countryName, + String cityName, + Long placeId, + String placeName, + Double latitude, + Double longitude, + Long tripRecordId, + List images, + List videos +) { + + @Builder + public TripRecordScheduleDetailResponseDto(Long id, Integer dayNumber, Integer ordering, + String content, ExternalLinkTagType tagType, String tagUrl, String countryName, + String cityName, + Long placeId, String placeName, Double latitude, Double longitude, Long tripRecordId, + List images, + List videos) { + this.id = id; + this.dayNumber = dayNumber; + this.ordering = ordering; + this.content = content; + this.tagType = tagType; + this.tagUrl = tagUrl; + this.countryName = countryName; + this.cityName = cityName; + this.placeId = placeId; + this.placeName = placeName; + this.latitude = latitude; + this.longitude = longitude; + this.tripRecordId = tripRecordId; + this.images = images; + this.videos = videos; + } + + public static TripRecordScheduleDetailResponseDto fromEntity(TripRecordSchedule entity) { + + List images = entity.getTripRecordScheduleImages(); + List imageDtos = images.stream() + .map(TripRecordScheduleImageResponseDto::fromEntity) + .toList(); + + List videos = entity.getTripRecordScheduleVideos(); + List videoDtos = videos.stream() + .map(TripRecordScheduleVideoResponseDto::fromEntity) + .toList(); + + + return TripRecordScheduleDetailResponseDto.builder() + .id(entity.getId()) + .dayNumber(entity.getDayNumber()) + .ordering(entity.getOrdering()) + .content(entity.getContent()) + .tagType(entity.getTagType()) + .tagUrl(entity.getTagUrl()) + .countryName(entity.getPlace().getCity().getCountry().getDescription()) + .cityName(entity.getPlace().getCity().getName()) + .placeId(entity.getPlace().getId()) + .placeName(entity.getPlace().getName()) + .latitude(entity.getPlace().getLatitude()) + .longitude(entity.getPlace().getLongitude()) + .tripRecordId(entity.getTripRecord() != null ? entity.getTripRecord().getId() : null) + .images(imageDtos) + .videos(videoDtos) + .build(); + } + + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordHotShortsListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordHotShortsListResponseDto.java new file mode 100644 index 00000000..2455a107 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordHotShortsListResponseDto.java @@ -0,0 +1,30 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordVideoMemberResponseDto; +import lombok.Builder; + +public record TripRecordHotShortsListResponseDto( + + Long tripRecordId, + String tripRecordTitle, + Integer tripRecordStoreCount, + Long videoId, + String thumbnailUrl, + String videoUrl, + TripRecordVideoMemberResponseDto member + +) { + + @Builder + public TripRecordHotShortsListResponseDto(Long tripRecordId, String tripRecordTitle, + Integer tripRecordStoreCount, Long videoId, String thumbnailUrl, String videoUrl, + TripRecordVideoMemberResponseDto member) { + this.tripRecordId = tripRecordId; + this.tripRecordTitle = tripRecordTitle; + this.tripRecordStoreCount = tripRecordStoreCount; + this.videoId = videoId; + this.thumbnailUrl = thumbnailUrl; + this.videoUrl = videoUrl; + this.member = member; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleImageListResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleImageListResponseDto.java new file mode 100644 index 00000000..e816ea0a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleImageListResponseDto.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media; + +import lombok.Builder; + +public record TripRecordScheduleImageListResponseDto( + Long tripRecordId, + String imageUrl, + Integer tripRecordStoreCount +) { + + @Builder + public TripRecordScheduleImageListResponseDto(Long tripRecordId, String imageUrl, + Integer tripRecordStoreCount) { + this.tripRecordId = tripRecordId; + this.imageUrl = imageUrl; + this.tripRecordStoreCount = tripRecordStoreCount; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleImageResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleImageResponseDto.java new file mode 100644 index 00000000..8c492a6f --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleImageResponseDto.java @@ -0,0 +1,25 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import lombok.Builder; + +public record TripRecordScheduleImageResponseDto( + Long id, + String imageUrl +) { + + @Builder + public TripRecordScheduleImageResponseDto(Long id, String imageUrl) { + this.id = id; + this.imageUrl = imageUrl; + } + + public static TripRecordScheduleImageResponseDto fromEntity(TripRecordScheduleImage entity) { + return TripRecordScheduleImageResponseDto.builder() + .id(entity.getId()) + .imageUrl(entity.getImageUrl()) + .build(); + } + + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoDetailDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoDetailDto.java new file mode 100644 index 00000000..ab3c9107 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoDetailDto.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media; + +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordMemberResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import lombok.Builder; + +public record TripRecordScheduleVideoDetailDto( + Long videoId, + String videoUrl, + Long tripRecordId, + String tripRecordTitle, + TripRecordMemberResponseDto member + +) { + + @Builder + public TripRecordScheduleVideoDetailDto(Long videoId, String videoUrl, Long tripRecordId, + String tripRecordTitle, TripRecordMemberResponseDto member) { + this.videoId = videoId; + this.videoUrl = videoUrl; + this.tripRecordId = tripRecordId; + this.tripRecordTitle = tripRecordTitle; + this.member = member; + } + + + public static TripRecordScheduleVideoDetailDto fromEntity(TripRecordScheduleVideo entity) { + + return TripRecordScheduleVideoDetailDto.builder() + .videoId(entity.getId()) + .videoUrl(entity.getVideoUrl()) + .tripRecordId(entity.getTripRecordSchedule().getTripRecord().getId()) + .tripRecordTitle(entity.getTripRecordSchedule().getTripRecord().getTitle()) + .member(TripRecordMemberResponseDto + .fromEntity(entity.getTripRecordSchedule().getTripRecord().getMember())) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoListItemResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoListItemResponseDto.java new file mode 100644 index 00000000..bd8417ca --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoListItemResponseDto.java @@ -0,0 +1,35 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleVideoQueryDto; +import lombok.Builder; + +public record TripRecordScheduleVideoListItemResponseDto( + Long videoId, + Long tripRecordId, + String tripRecordTitle, + String thumbnailUrl, + String videoUrl, + Integer storedCount, + Long memberId, + String memberName, + String profileImageUrl +) { + + @Builder + public TripRecordScheduleVideoListItemResponseDto { + } + + public static TripRecordScheduleVideoListItemResponseDto fromQueryDto(TripRecordScheduleVideoQueryDto dto) { + return TripRecordScheduleVideoListItemResponseDto.builder() + .videoId(dto.id()) + .tripRecordId(dto.tripRecordId()) + .tripRecordTitle(dto.tripRecordTitle()) + .thumbnailUrl(dto.thumbnailUrl()) + .videoUrl(dto.videoUrl()) + .storedCount(dto.storedCount()) + .memberId(dto.memberId()) + .memberName(dto.memberName()) + .profileImageUrl(dto.profileImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoResponseDto.java new file mode 100644 index 00000000..bdad81d1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/dto/response/triprecord_schedule_media/TripRecordScheduleVideoResponseDto.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import lombok.Builder; + +public record TripRecordScheduleVideoResponseDto( + Long id, + String videoUrl +) { + + @Builder + public TripRecordScheduleVideoResponseDto(Long id, String videoUrl) { + this.id = id; + this.videoUrl = videoUrl; + } + + public static TripRecordScheduleVideoResponseDto fromEntity(TripRecordScheduleVideo entity) { + return TripRecordScheduleVideoResponseDto.builder() + .id(entity.getId()) + .videoUrl(entity.getVideoUrl()) + .build(); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecord.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecord.java new file mode 100644 index 00000000..95f83478 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecord.java @@ -0,0 +1,163 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.store.entity.TripRecordStore; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.TripRecordRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecord extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_id") + private Long id; + + @Column(nullable = false) + private String title; + private String content; + + @Enumerated(EnumType.STRING) + private ExpenseRangeType expenseRangeType; + + private String countries; + + private LocalDate tripStartDay; + private LocalDate tripEndDay; + + private Integer totalDays; + private Double averageRating; + private Integer viewCount; + private Integer storeCount; + private Integer reviewCount; + private Integer commentCount; + + + @OneToMany(mappedBy = "tripRecord", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripRecordSchedules = new ArrayList<>(); + + @OneToMany(mappedBy = "tripRecord", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripRecordTags = new ArrayList<>(); + + @OneToMany(mappedBy = "tripRecord", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripRecordImages = new ArrayList<>(); + + @OneToMany(mappedBy = "tripRecord", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripRecordStores = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + @Builder + public TripRecord(Long id, String title, String content, ExpenseRangeType expenseRangeType, + String countries, LocalDate tripStartDay, LocalDate tripEndDay, Integer totalDays, + Double averageRating, Integer viewCount, Integer storeCount, Integer reviewCount, + Integer commentCount, List tripRecordSchedules, + List tripRecordTags, List tripRecordImages, + List tripRecordStores, Member member) { + this.id = id; + this.title = title; + this.content = content; + this.expenseRangeType = expenseRangeType; + this.countries = countries; + this.tripStartDay = tripStartDay; + this.tripEndDay = tripEndDay; + this.totalDays = totalDays; + this.averageRating = averageRating; + this.viewCount = viewCount; + this.storeCount = storeCount; + this.reviewCount = reviewCount; + this.commentCount = commentCount; + this.tripRecordSchedules = tripRecordSchedules; + this.tripRecordTags = tripRecordTags; + this.tripRecordImages = tripRecordImages; + this.tripRecordStores = tripRecordStores; + this.member = member; + } + + public void update(TripRecordRequestDto requestDto) { + this.title = requestDto.title(); + this.content = requestDto.content(); + this.expenseRangeType = requestDto.expenseRangeType(); + this.tripStartDay = requestDto.tripStartDay(); + this.tripEndDay = requestDto.tripEndDay(); + this.countries = requestDto.countries(); + } + + public void incrementViewCount() { + if(this.viewCount == null) { + this.viewCount = 1; + } else { + this.viewCount++; + } + } + + public void incrementStoreCount() { + if(this.storeCount == null) { + this.storeCount = 1; + } else { + this.storeCount++; + } + } + + public void decrementStoreCount() { + if(this.storeCount > 0) { + this.storeCount--; + } + } + + public void incrementReviewCount() { + if(this.reviewCount == null) { + this.reviewCount = 1; + } else { + this.reviewCount++; + } + } + + public void incrementCommentCount() { + if(this.commentCount == null) { + this.commentCount = 1; + } else { + this.commentCount++; + } + } + + public void decreaseCommentCount(int count) { + this.commentCount -= count; + } + + @PrePersist + public void prePersist() { + this.totalDays = calculateTotalDays(); + this.viewCount = 0; + this.storeCount = 0; + this.reviewCount = 0; + this.commentCount = 0; + } + + @PreUpdate + public void preUpdate() { + this.totalDays = calculateTotalDays(); + } + + private int calculateTotalDays() { + if (this.tripStartDay == null || this.tripEndDay == null) { + return 0; + } + return (int) ChronoUnit.DAYS.between(this.tripStartDay, this.tripEndDay); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordImage.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordImage.java new file mode 100644 index 00000000..0c1592fe --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordImage.java @@ -0,0 +1,47 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.TripRecordImageType; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordImage extends BaseTimeEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_image_id") + private Long id; + + private String imageUrl; + + @Enumerated(EnumType.STRING) + private ExternalLinkTagType tagType; + private String tagUrl; + + @ManyToOne + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @Builder + public TripRecordImage(TripRecordImageType imageType, String imageUrl, + ExternalLinkTagType tagType, String tagUrl, TripRecord tripRecord) { + this.imageUrl = imageUrl; + this.tagType = tagType; + this.tagUrl = tagUrl; + this.tripRecord = tripRecord; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordSchedule.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordSchedule.java new file mode 100644 index 00000000..14d345ce --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordSchedule.java @@ -0,0 +1,73 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordSchedule extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_schedule_id") + private Long id; + + @Column(nullable = false) + private Integer dayNumber; + + @Column(nullable = false) + private Integer ordering; + + private String content; + + @OneToMany(mappedBy = "tripRecordSchedule", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripRecordScheduleImages = new ArrayList<>(); + + @OneToMany(mappedBy = "tripRecordSchedule", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private List tripRecordScheduleVideos = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "place_id") + private Place place; + + @ManyToOne + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @Enumerated(EnumType.STRING) + private ExternalLinkTagType tagType; + + private String tagUrl; + + @Builder + public TripRecordSchedule(Integer dayNumber, Integer ordering, String content, + Place place, TripRecord tripRecord, ExternalLinkTagType tagType, String tagUrl) { + this.dayNumber = dayNumber; + this.ordering = ordering; + this.content = content; + this.place = place; + this.tripRecord = tripRecord; + this.tagType = tagType; + this.tagUrl = tagUrl; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordScheduleImage.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordScheduleImage.java new file mode 100644 index 00000000..40df91c7 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordScheduleImage.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordScheduleImage extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_schedule_image_id") + private Long id; + + private String imageUrl; + + @ManyToOne + @JoinColumn(name = "trip_schedule_id") + private TripRecordSchedule tripRecordSchedule; + + @Builder + public TripRecordScheduleImage(String imageUrl, + TripRecordSchedule tripRecordSchedule) { + this.imageUrl = imageUrl; + this.tripRecordSchedule = tripRecordSchedule; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordScheduleVideo.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordScheduleVideo.java new file mode 100644 index 00000000..326e0efa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordScheduleVideo.java @@ -0,0 +1,41 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + + +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordScheduleVideo extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_schedule_video_id") + private Long id; + + private String videoUrl; + private String thumbnailUrl; + + @ManyToOne + @JoinColumn(name = "trip_schedule_id") + private TripRecordSchedule tripRecordSchedule; + + @Builder + public TripRecordScheduleVideo(String videoUrl, String thumbnailUrl, + TripRecordSchedule tripRecordSchedule) { + this.videoUrl = videoUrl; + this.thumbnailUrl = thumbnailUrl; + this.tripRecordSchedule = tripRecordSchedule; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordStore.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordStore.java new file mode 100644 index 00000000..38e97e78 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordStore.java @@ -0,0 +1,40 @@ +//package com.haejwo.tripcometrue.domain.triprecord.entity; +// +//import com.haejwo.tripcometrue.domain.member.entity.Member; +//import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +//import jakarta.persistence.Column; +//import jakarta.persistence.Entity; +//import jakarta.persistence.GeneratedValue; +//import jakarta.persistence.GenerationType; +//import jakarta.persistence.Id; +//import jakarta.persistence.JoinColumn; +//import jakarta.persistence.ManyToOne; +//import lombok.AccessLevel; +//import lombok.Builder; +//import lombok.Getter; +//import lombok.NoArgsConstructor; +// +//@Entity +//@Getter +//@NoArgsConstructor(access = AccessLevel.PACKAGE) +//public class TripRecordStore extends BaseTimeEntity { +// +// @Id @GeneratedValue(strategy = GenerationType.IDENTITY) +// @Column(name = "trip_record_store_id") +// private Long id; +// +// @ManyToOne +// @JoinColumn(name = "member_id") +// private Member member; +// +// @ManyToOne +// @JoinColumn(name = "trip_record_id") +// private TripRecord tripRecord; +// +// @Builder +// public TripRecordStore(Long id, Member member, TripRecord tripRecord) { +// this.id = id; +// this.member = member; +// this.tripRecord = tripRecord; +// } +//} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordTag.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordTag.java new file mode 100644 index 00000000..2d8f2048 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordTag.java @@ -0,0 +1,37 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordTag extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_tag_id") + private Long id; + + private String hashTagType; + + @ManyToOne + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @Builder + public TripRecordTag(String hashTagType, TripRecord tripRecord) { + this.hashTagType = hashTagType; + this.tripRecord = tripRecord; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordViewCount.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordViewCount.java new file mode 100644 index 00000000..2c4ef029 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordViewCount.java @@ -0,0 +1,49 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; + +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordViewCount extends BaseTimeEntity { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_view_count_id") + private Long id; + + private Integer viewCount; + + private LocalDate date; + + @ManyToOne + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @Builder + public TripRecordViewCount(Long id, Integer viewCount, LocalDate date, TripRecord tripRecord) { + this.id = id; + this.viewCount = viewCount; + this.date = date; + this.tripRecord = tripRecord; + } + + public void incrementViewCount() { + if(this.viewCount == null) { + this.viewCount = 1; + } else { + this.viewCount++; + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordViewHistory.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordViewHistory.java new file mode 100644 index 00000000..40c5bbe6 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/TripRecordViewHistory.java @@ -0,0 +1,48 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.global.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PreUpdate; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TripRecordViewHistory extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "trip_record_view_history_id") + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "trip_record_id") + private TripRecord tripRecord; + + @PreUpdate + public void updateUpdatedAt() { + this.updatedAt = LocalDateTime.now(); + } + + @Builder + public TripRecordViewHistory(Member member, TripRecord tripRecord) { + this.member = member; + this.tripRecord = tripRecord; + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/ExpenseRangeType.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/ExpenseRangeType.java new file mode 100644 index 00000000..ef6137e1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/ExpenseRangeType.java @@ -0,0 +1,45 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.haejwo.tripcometrue.domain.triprecord.exception.ExpenseRangeTypeNotFoundException; +import java.util.Objects; +import java.util.stream.Stream; + +public enum ExpenseRangeType { + + BELOW_50(50), + BELOW_100(100), + BELOW_200(200), + BELOW_300(300), + ABOVE_300(Integer.MAX_VALUE); + + private final Integer max; + + ExpenseRangeType(Integer max) { + this.max = max; + } + + public static ExpenseRangeType findByMax(Integer max) { + return Stream.of(ExpenseRangeType.values()) + .filter(p -> Objects.equals(p.max, max)) + .findFirst() + .orElseThrow(ExpenseRangeTypeNotFoundException::new); + } + + @JsonCreator + public static ExpenseRangeType parse(String inputValue) { + return Stream.of(ExpenseRangeType.values()) + .filter(e -> { + String replacedInputValue = inputValue.replaceAll(" ", "") + .replaceAll("_", ""); + + String replacedExpenseRangeType = e.name().replaceAll("_", ""); + + return replacedExpenseRangeType.equals(replacedInputValue.toUpperCase()); + } + ) + .findFirst() + .orElse(null); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/ExternalLinkTagType.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/ExternalLinkTagType.java new file mode 100644 index 00000000..3d6d02f0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/ExternalLinkTagType.java @@ -0,0 +1,15 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity.type; + +public enum ExternalLinkTagType { + + AIRLINE_TICKET_PURCHASE("ํ•ญ๊ณต๊ถŒ ๊ตฌ๋งค ์ฃผ์†Œ"), + ACCOMMODATION_RESERVATION("์ˆ™๋ฐ•์‹œ์„ค ์˜ˆ์•ฝ ์ฃผ์†Œ"), + FOOD_TOURISM_LOCATION("์Œ์‹์ /๊ด€๊ด‘์‹œ์„ค ๋“ฑ์˜ ์œ„์น˜์ฃผ์†Œ"), + TICKET_PASS_PURCHASE("ํ‹ฐ์ผ“/์ž…์žฅ๊ถŒ/ํŒจ์Šค ๊ตฌ๋งค ์ฃผ์†Œ"); + + private final String name; + + ExternalLinkTagType(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/HashTagType.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/HashTagType.java new file mode 100644 index 00000000..606139fe --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/HashTagType.java @@ -0,0 +1,22 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity.type; + +public enum HashTagType { + + COUPLE_TRIP("#์—ฐ์ธ๋ผ๋ฆฌ"), + FRIEND_TRIP("#์นœ๊ตฌ๋ผ๋ฆฌ"), + SOLO_TRIP("#ํ˜ผ์ž์—ฌํ–‰"), + FAMILY_TRIP("#๊ฐ€์กฑ์—ฌํ–‰"), + BACKPACKING_TRIP("#๋ฐฐ๋‚ญ์—ฌํ–‰"), + PAKAGE_TRIP("#ํŒจํ‚ค์ง€์—ฌํ–‰"), + FREE_TRIP("#์ž์œ ์—ฌํ–‰"), + DAY_TRIP("#๋‹น์ผ์น˜๊ธฐ"), + LONG_TRIP("#๋‹น์ผ์น˜๊ธฐ"), + LOW_BUDGET("#์ €์˜ˆ์‚ฐ"); + + private final String hashtag; + + HashTagType(String hashtag) { + this.hashtag = hashtag; + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/TripRecordImageType.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/TripRecordImageType.java new file mode 100644 index 00000000..6ccde0da --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/entity/type/TripRecordImageType.java @@ -0,0 +1,8 @@ +package com.haejwo.tripcometrue.domain.triprecord.entity.type; + +public enum TripRecordImageType { + + IMAGE, + VIDEO + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/ExpenseRangeTypeNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/ExpenseRangeTypeNotFoundException.java new file mode 100644 index 00000000..ff35c715 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/ExpenseRangeTypeNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.triprecord.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class ExpenseRangeTypeNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.EXPENSE_RANGE_TYPE_NOT_FOUND; + + public ExpenseRangeTypeNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/TripRecordNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/TripRecordNotFoundException.java new file mode 100644 index 00000000..884afe37 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/TripRecordNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.triprecord.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripRecordNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_RECORD_NOT_FOUND; + + public TripRecordNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/TripRecordScheduleVideoNotFoundException.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/TripRecordScheduleVideoNotFoundException.java new file mode 100644 index 00000000..88d90ded --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/exception/TripRecordScheduleVideoNotFoundException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.triprecord.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class TripRecordScheduleVideoNotFoundException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.TRIP_RECORD_SCHEDULE_VIDEO_NOT_FOUND; + + public TripRecordScheduleVideoNotFoundException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepository.java new file mode 100644 index 00000000..5eba66ee --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepository.java @@ -0,0 +1,19 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordRepository extends + JpaRepository, + TripRecordRepositoryCustom + +{ + + Page findByMemberId(Long memberId, Pageable pageable); + + Optional findByMember(Member member); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepositoryCustom.java new file mode 100644 index 00000000..79b2ef9b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepositoryCustom.java @@ -0,0 +1,31 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordSearchParamAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordHotShortsListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public interface TripRecordRepositoryCustom { + + List finTripRecordWithFilter(Pageable pageable, TripRecordListRequestAttribute request); + + Slice findTripRecordsByFilter(TripRecordSearchParamAttribute requestParamAttribute, Pageable pageable); + + Slice findTripRecordsByHashtag(String hashTag, Pageable pageable); + + Slice findTripRecordsByExpenseRangeType(ExpenseRangeType expenseRangeType, Pageable pageable); + + List findTopTripRecordsDomestic(int size); + + List findTopTripRecordsOverseas(int size); + + List findTripRecordHotShortsList(Pageable pageable); + + List findTripRecordsWithMemberInMemberIds(List memberIds); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepositoryImpl.java new file mode 100644 index 00000000..e00427e0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord/TripRecordRepositoryImpl.java @@ -0,0 +1,398 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord; + +import static com.haejwo.tripcometrue.domain.city.entity.QCity.city; +import static com.haejwo.tripcometrue.domain.member.entity.QMember.member; +import static com.haejwo.tripcometrue.domain.place.entity.QPlace.place; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecord.tripRecord; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordSchedule.tripRecordSchedule; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordTag.tripRecordTag; + +import com.haejwo.tripcometrue.domain.member.entity.QMember; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordSearchParamAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordMemberResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.member.TripRecordVideoMemberResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordHotShortsListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordScheduleVideo; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordTag; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.global.enums.Country; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.NullExpression; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.OrderSpecifier.NullHandling; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import org.springframework.util.StringUtils; + +public class TripRecordRepositoryImpl extends QuerydslRepositorySupport implements TripRecordRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public TripRecordRepositoryImpl(JPAQueryFactory queryFactory) { + super(TripRecord.class); + this.queryFactory = queryFactory; + } + + @Override + public List finTripRecordWithFilter( + Pageable pageable, + TripRecordListRequestAttribute request + ) { + + QTripRecord qTripRecord = QTripRecord.tripRecord; + QTripRecordTag qTripRecordTag = QTripRecordTag.tripRecordTag; + QTripRecordSchedule qTripRecordSchedule = QTripRecordSchedule.tripRecordSchedule; + QTripRecordImage qTripRecordImage = QTripRecordImage.tripRecordImage; + QMember qMember = QMember.member; + + BooleanBuilder booleanBuilder = new BooleanBuilder(); + + // hashtag + if(request.hashtag() != null) { + booleanBuilder.and(qTripRecordTag.hashTagType.eq(request.hashtag())); + } + // placeId + if(request.placeId() != null) { + booleanBuilder.and(qTripRecordSchedule.place.id.eq(request.placeId())); + } + + // expenseRangeType + if(request.expenseRangeType() != null) { + ExpenseRangeType expenseRangeType = ExpenseRangeType.findByMax(request.expenseRangeType()); + booleanBuilder.and(qTripRecord.expenseRangeType.eq(expenseRangeType)); + } + + // totalDays + if(request.totalDays() != null) { + booleanBuilder.and(qTripRecord.totalDays.eq(request.totalDays())); + } + + OrderSpecifier orderSpecifier = new OrderSpecifier<>(Order.ASC, qTripRecord.id); + + if (request.orderBy() != null) { + switch (request.orderBy()) { + case "averageRating": + orderSpecifier = new OrderSpecifier<>(request.order(), qTripRecord.averageRating); + break; + case "viewCount": + orderSpecifier = new OrderSpecifier<>(request.order(), qTripRecord.viewCount); + break; + case "storeCount": + orderSpecifier = new OrderSpecifier<>(request.order(), qTripRecord.storeCount); + break; + case "reviewCount": + orderSpecifier = new OrderSpecifier<>(request.order(), qTripRecord.reviewCount); + break; + case "commentCount": + orderSpecifier = new OrderSpecifier<>(request.order(), qTripRecord.commentCount); + break; + default: + throw new IllegalArgumentException("Invalid orderBy parameter: " + request.orderBy()); // TODO: ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๋งŒ๋“ค๊ธฐ + } + } + + List result = from(qTripRecord) + .leftJoin(qTripRecord.tripRecordTags, qTripRecordTag) + .leftJoin(qTripRecord.tripRecordSchedules, qTripRecordSchedule) + .leftJoin(qTripRecord.tripRecordImages, qTripRecordImage) + .join(qTripRecord.member, qMember) + .where(booleanBuilder) + .groupBy(qTripRecord) + .orderBy(orderSpecifier) + .select(Projections.constructor(TripRecordListResponseDto.class, + qTripRecord.id, + qTripRecord.title, + qTripRecord.countries, + qTripRecord.totalDays, + qTripRecord.commentCount, + qTripRecord.storeCount, + qTripRecord.averageRating, + JPAExpressions + .select(qTripRecordImage.imageUrl.min()) + .from(qTripRecordImage) + .where(qTripRecordImage.tripRecord.id.eq(qTripRecord.id)), + Projections.constructor(TripRecordMemberResponseDto.class, qMember.memberBase.nickname, qMember.profileImage) + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return result; + } + + @Override + public Slice findTripRecordsByFilter( + TripRecordSearchParamAttribute requestParamAttribute, + Pageable pageable + ) { + + List tripRecordIds = queryFactory + .select(tripRecord.id) + .from(tripRecordSchedule) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecordSchedule.place, place) + .join(place.city, city) + .where( + eqCityId(requestParamAttribute.cityId()), + containsIgnoreCaseCityName(requestParamAttribute.cityName()), + containsIgnoreCasePlaceName(requestParamAttribute.placeName()), + eqOrGoeTotalDays(requestParamAttribute.totalDays()) + ) + .groupBy(tripRecord.id) + .fetch(); + + int pageSize = pageable.getPageSize(); + List content = queryFactory + .selectFrom(tripRecord) + .join(tripRecord.member, member).fetchJoin() + .where( + tripRecord.id.in(tripRecordIds) + ) + .orderBy(getSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice findTripRecordsByHashtag( + String hashTag, Pageable pageable + ) { + + int pageSize = pageable.getPageSize(); + List content = queryFactory + .selectFrom(tripRecord) + .where( + tripRecord.id.in( + JPAExpressions.select(tripRecord.id) + .from(tripRecordTag) + .join(tripRecordTag.tripRecord, tripRecord) + .where( + containsIgnoreCaseHashTag(hashTag) + ) + .groupBy(tripRecord.id) + ) + ) + .orderBy(getSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice findTripRecordsByExpenseRangeType( + ExpenseRangeType expenseRangeType, Pageable pageable + ) { + int pageSize = pageable.getPageSize(); + List content = queryFactory + .selectFrom(tripRecord) + .where( + eqExpenseRangeType(expenseRangeType) + ) + .orderBy(getSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public List findTopTripRecordsDomestic(int size) { + + return queryFactory + .selectFrom(tripRecord) + .where(tripRecord.countries.containsIgnoreCase(Country.KOREA.name())) + .orderBy(tripRecord.averageRating.desc(), tripRecord.storeCount.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findTopTripRecordsOverseas(int size) { + + return queryFactory + .selectFrom(tripRecord) + .where(tripRecord.countries.containsIgnoreCase(Country.KOREA.name()).not()) + .orderBy(tripRecord.averageRating.desc(), tripRecord.storeCount.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findTripRecordHotShortsList(Pageable pageable) { + + QTripRecord qTripRecord = QTripRecord.tripRecord; + QTripRecordSchedule qTripRecordSchedule = QTripRecordSchedule.tripRecordSchedule; + QTripRecordScheduleVideo qTripRecordScheduleVideo = QTripRecordScheduleVideo.tripRecordScheduleVideo; + QMember qMember = QMember.member; + + List result = queryFactory + .select(Projections.constructor(TripRecordHotShortsListResponseDto.class, + qTripRecord.id, + qTripRecord.title, + qTripRecord.storeCount, + qTripRecordScheduleVideo.id, + qTripRecordScheduleVideo.thumbnailUrl, + qTripRecordScheduleVideo.videoUrl, + Projections.constructor(TripRecordVideoMemberResponseDto.class, + qMember.id, + qMember.memberBase.nickname, + qMember.profileImage))) + .from(qTripRecord) + .leftJoin(qTripRecord.tripRecordSchedules, qTripRecordSchedule) + .leftJoin(qTripRecordSchedule.tripRecordScheduleVideos, qTripRecordScheduleVideo) + .leftJoin(qTripRecord.member, qMember) + .where(qTripRecordScheduleVideo.thumbnailUrl.isNotNull(), + qTripRecordScheduleVideo.id.eq( + JPAExpressions + .select(qTripRecordScheduleVideo.id.min()) + .from(qTripRecordScheduleVideo) + .where(qTripRecordScheduleVideo.tripRecordSchedule.tripRecord.id.eq(qTripRecord.id)))) + .orderBy(qTripRecord.storeCount.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return result; + } + + @Override + public List findTripRecordsWithMemberInMemberIds(List memberIds) { + + return queryFactory.selectFrom(tripRecord) + .join(tripRecord.member, member).fetchJoin() + .where(member.id.in(memberIds)) + .orderBy(tripRecord.storeCount.desc(), tripRecord.createdAt.desc()) + .fetch(); + } + + private BooleanExpression eqCityId(Long cityId) { + return Objects.nonNull(cityId) ? city.id.eq(cityId) : null; + } + + private BooleanExpression containsIgnoreCaseCityName(String cityName) { + if (!StringUtils.hasText(cityName)) { + return null; + } + + String replacedWhitespace = cityName.replaceAll(" ", ""); + + return Expressions.stringTemplate( + "function('replace',{0},{1},{2})", city.name, " ", "" + ).containsIgnoreCase(replacedWhitespace); + } + + private BooleanExpression containsIgnoreCasePlaceName(String placeName) { + if (!StringUtils.hasText(placeName)) { + return null; + } + + String replacedWhitespace = placeName.replaceAll(" ", ""); + + return Expressions.stringTemplate( + "function('replace',{0},{1},{2})", place.name, " ", "" + ).containsIgnoreCase(replacedWhitespace); + } + + private BooleanExpression containsIgnoreCaseHashTag(String hashTag) { + if (!StringUtils.hasText(hashTag)) { + return null; + } + + String replacedWhitespace = hashTag.replaceAll(" ", ""); + + return Expressions.stringTemplate( + "function('replace',{0},{1},{2})", tripRecordTag.hashTagType, " ", "" + ).containsIgnoreCase(replacedWhitespace); + } + + private BooleanExpression eqOrGoeTotalDays(String totalDays) { + if (!StringUtils.hasText(totalDays)) { + return null; + } + + if (totalDays.equalsIgnoreCase("etc")) { + return tripRecord.totalDays.goe(5); + } + + return tripRecord.totalDays.eq(Integer.parseInt(totalDays)); + } + + private BooleanExpression eqExpenseRangeType(ExpenseRangeType expenseRangeType) { + return Objects.nonNull(expenseRangeType) ? tripRecord.expenseRangeType.eq(expenseRangeType) : null; + } + + private OrderSpecifier[] getSort(Pageable pageable) { + + List> orderSpecifiers = new LinkedList<>(); + if (!pageable.getSort().isEmpty()) { + for (Sort.Order sortOrder : pageable.getSort()) { + Order direction = sortOrder.getDirection().isAscending() ? Order.ASC : Order.DESC; + + String property = sortOrder.getProperty(); + switch (property) { + case "id": + orderSpecifiers.add(new OrderSpecifier<>(direction, tripRecord.id)); + break; + case "createdAt": + orderSpecifiers.add(new OrderSpecifier<>(direction, tripRecord.createdAt)); + break; + case "storeCount": + orderSpecifiers.add(new OrderSpecifier<>(direction, tripRecord.storeCount)); + break; + case "averageRating": + orderSpecifiers.add(new OrderSpecifier<>(direction, tripRecord.averageRating)); + break; + } + } + } + + // ์ •๋ ฌ ๊ธฐ์ค€ ์—†๋Š” ๊ฒฝ์šฐ order by null + if(orderSpecifiers.isEmpty()) { + orderSpecifiers.add(new OrderSpecifier(Order.ASC, NullExpression.DEFAULT, NullHandling.Default)); + } + + return orderSpecifiers.toArray(OrderSpecifier[]::new); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_image/TripRecordImageRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_image/TripRecordImageRepository.java new file mode 100644 index 00000000..32babfc0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_image/TripRecordImageRepository.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_image; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordImageRepository extends JpaRepository { + + void deleteAllByTripRecordId(Long recordId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepository.java new file mode 100644 index 00000000..51da6c63 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepository.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordScheduleRepository extends + JpaRepository, + TripRecordScheduleRepositoryCustom { + + + List findAllByTripRecordId(Long recordId); + void deleteAllByTripRecordId(Long recordId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepositoryCustom.java new file mode 100644 index 00000000..66a782df --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepositoryCustom.java @@ -0,0 +1,12 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordScheduleImageListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleImageListResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface TripRecordScheduleRepositoryCustom { + + Page findScheduleImagesWithFilter(Pageable pageable, TripRecordScheduleImageListRequestAttribute request); + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepositoryImpl.java new file mode 100644 index 00000000..26c9904e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule/TripRecordScheduleRepositoryImpl.java @@ -0,0 +1,79 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordScheduleImageListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleImageListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordScheduleImage; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +public class TripRecordScheduleRepositoryImpl implements TripRecordScheduleRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + @Autowired + public TripRecordScheduleRepositoryImpl(EntityManager entityManager) { + this.jpaQueryFactory = new JPAQueryFactory(entityManager); + } + + @Override + public Page findScheduleImagesWithFilter( + Pageable pageable, + TripRecordScheduleImageListRequestAttribute request + ) { + + QTripRecord qTripRecord = QTripRecord.tripRecord; + QTripRecordSchedule qTripRecordSchedule = QTripRecordSchedule.tripRecordSchedule; + QTripRecordScheduleImage qTripRecordScheduleImage = QTripRecordScheduleImage.tripRecordScheduleImage; + + BooleanBuilder booleanBuilder = new BooleanBuilder(); + + if(request.placeId() != null) { + booleanBuilder.and(qTripRecordSchedule.place.id.eq(request.placeId())); + } + + OrderSpecifier orderSpecifier = new OrderSpecifier<>(Order.DESC, qTripRecord.storeCount); + + if (request.orderBy() != null) { + switch (request.orderBy()) { + case "storeCount": + orderSpecifier = new OrderSpecifier<>(Order.DESC, qTripRecord.storeCount); + break; + case "createdAt": + orderSpecifier = new OrderSpecifier<>(Order.DESC, qTripRecord.createdAt); + break; + } + } + + QueryResults queryResults = jpaQueryFactory + .select(Projections.constructor(TripRecordScheduleImageListResponseDto.class, + qTripRecord.id, + qTripRecordScheduleImage.imageUrl.min(), + qTripRecord.storeCount)) + .from(qTripRecordSchedule) + .leftJoin(qTripRecordSchedule.tripRecord, qTripRecord) + .leftJoin(qTripRecordSchedule.tripRecordScheduleImages, qTripRecordScheduleImage) + .where(booleanBuilder) + .groupBy(qTripRecordSchedule.tripRecord.id) + .orderBy(orderSpecifier) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetchResults(); + + Page result = new PageImpl<>(queryResults.getResults(), pageable, queryResults.getTotal()); + + return result; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepository.java new file mode 100644 index 00000000..421b6241 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepository.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordScheduleImageRepository extends + JpaRepository, TripRecordScheduleImageRepositoryCustom { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryCustom.java new file mode 100644 index 00000000..2e52158d --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryCustom.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleImageWithPlaceIdQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public interface TripRecordScheduleImageRepositoryCustom { + + Slice findByCityId(Long cityId, Pageable pageable); + + List findByCityIdOrderByCreatedAtDescLimitSize(Long cityId, Integer size); + + List findInPlaceIdsOrderByCreatedAtDesc(List placeIds); + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryImpl.java new file mode 100644 index 00000000..23d1b02a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryImpl.java @@ -0,0 +1,112 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleImageWithPlaceIdQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.haejwo.tripcometrue.domain.city.entity.QCity.city; +import static com.haejwo.tripcometrue.domain.place.entity.QPlace.place; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordSchedule.tripRecordSchedule; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordScheduleImage.tripRecordScheduleImage; + +@RequiredArgsConstructor +public class TripRecordScheduleImageRepositoryImpl implements TripRecordScheduleImageRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Slice findByCityId(Long cityId, Pageable pageable) { + + int pageSize = pageable.getPageSize(); + List content = queryFactory + .selectFrom(tripRecordScheduleImage) + .join(tripRecordScheduleImage.tripRecordSchedule, tripRecordSchedule).fetchJoin() + .join(tripRecordSchedule.place, place) + .join(place.city, city) + .where( + city.id.eq(cityId) + ) + .orderBy(getSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public List findByCityIdOrderByCreatedAtDescLimitSize(Long cityId, Integer size) { + return queryFactory + .selectFrom(tripRecordScheduleImage) + .join(tripRecordScheduleImage.tripRecordSchedule, tripRecordSchedule).fetchJoin() + .join(tripRecordSchedule.place, place) + .join(place.city, city) + .where( + city.id.eq(cityId) + ) + .orderBy(tripRecordScheduleImage.createdAt.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findInPlaceIdsOrderByCreatedAtDesc(List placeIds) { + return queryFactory + .select( + Projections.constructor( + TripRecordScheduleImageWithPlaceIdQueryDto.class, + tripRecordSchedule.place.id.as("placeId"), + tripRecordScheduleImage.imageUrl, + tripRecordScheduleImage.createdAt + ) + ) + .from(tripRecordScheduleImage) + .join(tripRecordScheduleImage.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.place, place) + .where(place.id.in(placeIds)) + .orderBy(tripRecordScheduleImage.createdAt.desc()) + .fetch(); + } + + private OrderSpecifier[] getSort(Pageable pageable) { + + OrderSpecifier newest = new OrderSpecifier<>(Order.DESC, tripRecordScheduleImage.createdAt); + //์„œ๋น„์Šค์—์„œ ๋ณด๋‚ด์ค€ Pageable ๊ฐ์ฒด์— ์ •๋ ฌ์กฐ๊ฑด null ๊ฐ’ ์ฒดํฌ + if (!pageable.getSort().isEmpty()) { + //์ •๋ ฌ๊ฐ’์ด ๋“ค์–ด ์žˆ์œผ๋ฉด for ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค + for (Sort.Order sortOrder : pageable.getSort()) { + // ์„œ๋น„์Šค์—์„œ ๋„ฃ์–ด์ค€ DESC or ASC ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. + com.querydsl.core.types.Order direction = sortOrder.getDirection().isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC; + // ์„œ๋น„์Šค์—์„œ ๋„ฃ์–ด์ค€ ์ •๋ ฌ ์กฐ๊ฑด์„ ์Šค์œ„์น˜ ์ผ€์ด์Šค ๋ฌธ์„ ํ™œ์šฉํ•˜์—ฌ ์…‹ํŒ…ํ•˜์—ฌ ์ค€๋‹ค. + String property = sortOrder.getProperty(); + switch (property) { + case "id": + return new OrderSpecifier[]{new OrderSpecifier<>(direction, tripRecordScheduleImage.id)}; + case "createdAt": + return new OrderSpecifier[]{new OrderSpecifier<>(direction, tripRecordScheduleImage.createdAt)}; + case "storedCount": + return new OrderSpecifier[]{new OrderSpecifier<>(direction, tripRecordSchedule.tripRecord.storeCount), newest}; + } + } + } + + return new OrderSpecifier[]{newest}; // ์ตœ์‹ ์ˆœ + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepository.java new file mode 100644 index 00000000..586235ba --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepository.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordScheduleVideoRepository extends + JpaRepository, TripRecordScheduleVideoRepositoryCustom { + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryCustom.java new file mode 100644 index 00000000..f07977dd --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryCustom.java @@ -0,0 +1,22 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleVideoQueryDto; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public interface TripRecordScheduleVideoRepositoryCustom { + + Slice findByCityId(Long cityId, Pageable pageable); + + List findByCityIdOrderByCreatedAtDescLimitSize(Long cityId, Integer size); + + List findNewestVideos(int size); + + List findNewestVideosDomestic(int size); + + List findNewestVideosOverseas(int size); + + List findInMemberIds(List memberIds); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryImpl.java new file mode 100644 index 00000000..66cf7ad2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryImpl.java @@ -0,0 +1,231 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleVideoQueryDto; +import com.haejwo.tripcometrue.global.enums.Country; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.haejwo.tripcometrue.domain.city.entity.QCity.city; +import static com.haejwo.tripcometrue.domain.member.entity.QMember.member; +import static com.haejwo.tripcometrue.domain.place.entity.QPlace.place; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecord.tripRecord; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordSchedule.tripRecordSchedule; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordScheduleVideo.tripRecordScheduleVideo; + +@RequiredArgsConstructor +public class TripRecordScheduleVideoRepositoryImpl implements TripRecordScheduleVideoRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Slice findByCityId(Long cityId, Pageable pageable) { + + int pageSize = pageable.getPageSize(); + List content = queryFactory + .select( + Projections.constructor( + TripRecordScheduleVideoQueryDto.class, + tripRecordScheduleVideo.id, + tripRecord.id, + tripRecord.title, + tripRecordScheduleVideo.thumbnailUrl, + tripRecordScheduleVideo.videoUrl, + tripRecord.storeCount, + member.id, + member.memberBase.nickname, + member.profileImage + ) + ) + .from(tripRecordScheduleVideo) + .join(tripRecordScheduleVideo.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.place, place) + .join(place.city, city) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecord.member, member) + .where( + city.id.eq(cityId) + ) + .orderBy(getSort(pageable)) + .offset(pageable.getOffset()) + .limit(pageSize + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageSize) { + content.remove(pageSize); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public List findByCityIdOrderByCreatedAtDescLimitSize(Long cityId, Integer size) { + return queryFactory + .select( + Projections.constructor( + TripRecordScheduleVideoQueryDto.class, + tripRecordScheduleVideo.id, + tripRecord.id, + tripRecord.title, + tripRecordScheduleVideo.thumbnailUrl, + tripRecordScheduleVideo.videoUrl, + tripRecord.storeCount, + member.id, + member.memberBase.nickname, + member.profileImage + ) + ) + .from(tripRecordScheduleVideo) + .join(tripRecordScheduleVideo.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.place, place) + .join(place.city, city) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecord.member, member) + .where( + city.id.eq(cityId) + ) + .limit(size) + .fetch(); + } + + @Override + public List findNewestVideos(int size) { + return queryFactory + .select( + Projections.constructor( + TripRecordScheduleVideoQueryDto.class, + tripRecordScheduleVideo.id, + tripRecord.id, + tripRecord.title, + tripRecordScheduleVideo.thumbnailUrl, + tripRecordScheduleVideo.videoUrl, + tripRecord.storeCount, + member.id, + member.memberBase.nickname, + member.profileImage + ) + ) + .from(tripRecordScheduleVideo) + .join(tripRecordScheduleVideo.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecord.member, member) + .orderBy(tripRecordScheduleVideo.createdAt.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findNewestVideosDomestic(int size) { + return queryFactory + .select( + Projections.constructor( + TripRecordScheduleVideoQueryDto.class, + tripRecordScheduleVideo.id, + tripRecord.id, + tripRecord.title, + tripRecordScheduleVideo.thumbnailUrl, + tripRecordScheduleVideo.videoUrl, + tripRecord.storeCount, + member.id, + member.memberBase.nickname, + member.profileImage + ) + ) + .from(tripRecordScheduleVideo) + .join(tripRecordScheduleVideo.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecord.member, member) + .where(tripRecord.countries.containsIgnoreCase(Country.KOREA.name())) + .orderBy(tripRecordScheduleVideo.createdAt.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findNewestVideosOverseas(int size) { + return queryFactory + .select( + Projections.constructor( + TripRecordScheduleVideoQueryDto.class, + tripRecordScheduleVideo.id, + tripRecord.id, + tripRecord.title, + tripRecordScheduleVideo.thumbnailUrl, + tripRecordScheduleVideo.videoUrl, + tripRecord.storeCount, + member.id, + member.memberBase.nickname, + member.profileImage + ) + ) + .from(tripRecordScheduleVideo) + .join(tripRecordScheduleVideo.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecord.member, member) + .where(tripRecord.countries.containsIgnoreCase(Country.KOREA.name()).not()) + .orderBy(tripRecordScheduleVideo.createdAt.desc()) + .limit(size) + .fetch(); + } + + @Override + public List findInMemberIds(List memberIds) { + return queryFactory + .select( + Projections.constructor( + TripRecordScheduleVideoQueryDto.class, + tripRecordScheduleVideo.id, + tripRecord.id, + tripRecord.title, + tripRecordScheduleVideo.thumbnailUrl, + tripRecordScheduleVideo.videoUrl, + tripRecord.storeCount, + member.id, + member.memberBase.nickname, + member.profileImage + ) + ) + .from(tripRecordScheduleVideo) + .join(tripRecordScheduleVideo.tripRecordSchedule, tripRecordSchedule) + .join(tripRecordSchedule.tripRecord, tripRecord) + .join(tripRecord.member, member) + .where(member.id.in(memberIds)) + .orderBy(tripRecordScheduleVideo.createdAt.desc()) + .fetch(); + } + + private OrderSpecifier[] getSort(Pageable pageable) { + OrderSpecifier newest = new OrderSpecifier<>(Order.DESC, tripRecordScheduleVideo.createdAt); + //์„œ๋น„์Šค์—์„œ ๋ณด๋‚ด์ค€ Pageable ๊ฐ์ฒด์— ์ •๋ ฌ์กฐ๊ฑด null ๊ฐ’ ์ฒดํฌ + if (!pageable.getSort().isEmpty()) { + //์ •๋ ฌ๊ฐ’์ด ๋“ค์–ด ์žˆ์œผ๋ฉด for ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค + for (Sort.Order sortOrder : pageable.getSort()) { + // ์„œ๋น„์Šค์—์„œ ๋„ฃ์–ด์ค€ DESC or ASC ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. + com.querydsl.core.types.Order direction = sortOrder.getDirection().isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC; + // ์„œ๋น„์Šค์—์„œ ๋„ฃ์–ด์ค€ ์ •๋ ฌ ์กฐ๊ฑด์„ ์Šค์œ„์น˜ ์ผ€์ด์Šค ๋ฌธ์„ ํ™œ์šฉํ•˜์—ฌ ์…‹ํŒ…ํ•˜์—ฌ ์ค€๋‹ค. + String property = sortOrder.getProperty(); + switch (property) { + case "id": + return new OrderSpecifier[] {new OrderSpecifier<>(direction, tripRecordScheduleVideo.id)}; + case "createdAt": + return new OrderSpecifier[] {new OrderSpecifier<>(direction, tripRecordScheduleVideo.createdAt)}; + case "storedCount": + return new OrderSpecifier[] {new OrderSpecifier<>(direction, tripRecord.storeCount), newest}; + } + } + } + + return new OrderSpecifier[] {newest}; // ์ตœ์‹ ์ˆœ + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_tag/TripRecordTagRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_tag/TripRecordTagRepository.java new file mode 100644 index 00000000..6bfa0747 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_tag/TripRecordTagRepository.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_tag; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordTagRepository extends JpaRepository { + + void deleteAllByTripRecordId(Long TripRecordId); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_viewcount/TripRecordViewCountRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_viewcount/TripRecordViewCountRepository.java new file mode 100644 index 00000000..651a8c53 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_viewcount/TripRecordViewCountRepository.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_viewcount; + +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordViewCount; +import java.time.LocalDate; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TripRecordViewCountRepository extends JpaRepository { + + Optional findByTripRecordAndDate(TripRecord tripRecord, LocalDate date); + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordEditService.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordEditService.java new file mode 100644 index 00000000..354ff86a --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordEditService.java @@ -0,0 +1,212 @@ +package com.haejwo.tripcometrue.domain.triprecord.service; + +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.exception.PlaceNotFoundException; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.CreateSchedulePlaceRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.TripRecordRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.TripRecordScheduleRequestDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.GetCountryCityResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.GetCountryResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule.SearchScheduleTripResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordTag; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_image.TripRecordImageRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule.TripRecordScheduleRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image.TripRecordScheduleImageRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video.TripRecordScheduleVideoRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_tag.TripRecordTagRepository; +import com.haejwo.tripcometrue.global.enums.Continent; +import com.haejwo.tripcometrue.global.enums.Country; +import com.haejwo.tripcometrue.global.exception.PermissionDeniedException; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TripRecordEditService { + + private final TripRecordRepository tripRecordRepository; + private final TripRecordImageRepository tripRecordImageRepository; + private final TripRecordTagRepository tripRecordTagRepository; + private final TripRecordScheduleRepository tripRecordScheduleRepository; + private final TripRecordScheduleImageRepository tripRecordScheduleImageRepository; + private final TripRecordScheduleVideoRepository tripRecordScheduleVideoRepository; + private final CityRepository cityRepository; + private final PlaceRepository placeRepository; + + @Transactional + public void addTripRecord(PrincipalDetails principalDetails, TripRecordRequestDto requestDto) { + validatePlaceId(requestDto.tripRecordSchedules()); + TripRecord requestTripRecord = requestDto.toEntity(principalDetails.getMember()); + tripRecordRepository.save(requestTripRecord); + + saveTripRecordImages(requestDto, requestTripRecord); + saveHashTags(requestDto, requestTripRecord); + saveTripRecordSchedules(requestDto, requestTripRecord); + } + + private void saveTripRecordImages(TripRecordRequestDto requestDto, + TripRecord requestTripRecord) { + requestDto.tripRecordImages().forEach(tripRecordImageRequestDto -> { + TripRecordImage tripRecordImage = tripRecordImageRequestDto.toEntity(requestTripRecord); + tripRecordImageRepository.save(tripRecordImage); + }); + } + + private void saveHashTags(TripRecordRequestDto requestDto, TripRecord requestTripRecord) { + requestDto.hashTags().forEach(hashTag -> { + TripRecordTag tripRecordTag = TripRecordTag.builder().hashTagType(hashTag) + .tripRecord(requestTripRecord).build(); + tripRecordTagRepository.save(tripRecordTag); + }); + } + + private void saveTripRecordSchedules(TripRecordRequestDto requestDto, + TripRecord requestTripRecord) { + requestDto.tripRecordSchedules().forEach(tripRecordScheduleRequestDto -> { + Place place = placeRepository.findById(tripRecordScheduleRequestDto.placeId()) + .orElseThrow(PlaceNotFoundException::new); + + TripRecordSchedule tripRecordSchedule = tripRecordScheduleRequestDto.toEntity( + requestTripRecord, place); + tripRecordScheduleRepository.save(tripRecordSchedule); + + saveTripRecordScheduleImages(tripRecordScheduleRequestDto, tripRecordSchedule); + saveTripRecordScheduleVideos(tripRecordScheduleRequestDto, tripRecordSchedule); + }); + } + + private void saveTripRecordScheduleImages(TripRecordScheduleRequestDto requestDto, + TripRecordSchedule tripRecordSchedule) { + requestDto.tripRecordScheduleImages().forEach(tripRecordScheduleImageUrl -> { + TripRecordScheduleImage tripRecordImage = TripRecordScheduleImage.builder() + .imageUrl(tripRecordScheduleImageUrl).tripRecordSchedule(tripRecordSchedule) + .build(); + tripRecordScheduleImageRepository.save(tripRecordImage); + }); + } + + private void saveTripRecordScheduleVideos(@NotNull TripRecordScheduleRequestDto requestDto, + TripRecordSchedule tripRecordSchedule) { + requestDto.tripRecordScheduleVideos().forEach(tripRecordScheduleVideoUrl -> { + TripRecordScheduleVideo tripRecordScheduleVideo = TripRecordScheduleVideo.builder() + .videoUrl(tripRecordScheduleVideoUrl).tripRecordSchedule(tripRecordSchedule) + .build(); + tripRecordScheduleVideoRepository.save(tripRecordScheduleVideo); + }); + } + + public List searchSchedulePlace(Country country, String city) { + return cityRepository.findByNameAndCountry(city, country).map( + foundCity -> placeRepository.findByCityId(foundCity.getId()) + .stream().map(SearchScheduleTripResponseDto::fromEntity) + .collect(Collectors.toList())).orElseGet(() -> new ArrayList<>()); + } + + public Long createSchedulePlace(CreateSchedulePlaceRequestDto createSchedulePlaceRequestDto) { + City city = cityRepository.findByNameAndCountry(createSchedulePlaceRequestDto.cityname(), + createSchedulePlaceRequestDto.country()).orElseThrow(CityNotFoundException::new); + + return placeRepository.save(createSchedulePlaceRequestDto.toEntity(city)).getId(); + } + + @Transactional + public void deleteTripRecord(PrincipalDetails principalDetails, Long tripRecordId) { + + Optional tripRecord = tripRecordRepository.findById(tripRecordId); + + if (tripRecord.isPresent()) { + TripRecord foundTripRecord = tripRecord.get(); + if (foundTripRecord.getMember().getId().equals(principalDetails.getMember().getId())) { + tripRecordRepository.delete(foundTripRecord); + } else { + throw new PermissionDeniedException(); + } + } else { + throw new TripRecordNotFoundException(); + } + } + + @Transactional + public void modifyTripRecord(PrincipalDetails principalDetails, + TripRecordRequestDto requestDto, Long tripRecordId) { + + validatePlaceId(requestDto.tripRecordSchedules()); + Optional tripRecord = tripRecordRepository.findById(tripRecordId); + + if (tripRecord.isPresent()) { + TripRecord foundTripRecord = tripRecord.get(); + if (foundTripRecord.getMember().getId().equals(principalDetails.getMember().getId())) { + + foundTripRecord.update(requestDto); + tripRecordRepository.save(foundTripRecord); + + deleteTripRecordAssociations(foundTripRecord); + saveTripRecordImages(requestDto, foundTripRecord); + saveHashTags(requestDto, foundTripRecord); + saveTripRecordSchedules(requestDto, foundTripRecord); + + } else { + throw new PermissionDeniedException(); + } + } else { + throw new TripRecordNotFoundException(); + } + } + + private void deleteTripRecordAssociations(TripRecord foundTripRecord) { + tripRecordImageRepository.deleteAllByTripRecordId(foundTripRecord.getId()); + tripRecordTagRepository.deleteAllByTripRecordId(foundTripRecord.getId()); + tripRecordScheduleRepository.deleteAllByTripRecordId(foundTripRecord.getId()); + } + + private void validatePlaceId( + List tripRecordScheduleRequestDtoList) { + for (TripRecordScheduleRequestDto tripRecordScheduleRequestDto : tripRecordScheduleRequestDtoList) { + placeRepository.findById(tripRecordScheduleRequestDto.placeId()) + .orElseThrow(PlaceNotFoundException::new); + } + } + + public List getCountryCity(Continent continent) { + + List responseDtoList = new ArrayList<>(); + + for (Country country : Country.values()) { + + if (continent == null || country.getContinent().equals(continent)) { + List cityList = new ArrayList<>(); + cityRepository.findAllByCountry(country).forEach(city -> { + cityList.add(GetCountryCityResponseDto.fromEntity(city)); + }); + + responseDtoList.add(new GetCountryResponseDto( + country.getContinent(), + country, + country.getDescription(), + country.getImageUrl(), + cityList + )); + } + } + + return responseDtoList; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordScheduleService.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordScheduleService.java new file mode 100644 index 00000000..0fab3422 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordScheduleService.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.domain.triprecord.service; + +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordScheduleImageListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleImageListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule.TripRecordScheduleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TripRecordScheduleService { + + private final TripRecordScheduleRepository tripRecordScheduleRepository; + + public Page findScheduleImages( + Pageable pageable, + TripRecordScheduleImageListRequestAttribute requestParam + ) { + + Page responseDtos = tripRecordScheduleRepository.findScheduleImagesWithFilter(pageable, requestParam); + + return responseDtos; + + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordScheduleVideoService.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordScheduleVideoService.java new file mode 100644 index 00000000..3b4168a8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordScheduleVideoService.java @@ -0,0 +1,47 @@ +package com.haejwo.tripcometrue.domain.triprecord.service; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleVideoQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video.TripRecordScheduleVideoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@RequiredArgsConstructor +@Service +public class TripRecordScheduleVideoService { + + private final TripRecordScheduleVideoRepository tripRecordScheduleVideoRepository; + + private static final int HOME_CONTENT_SIZE = 5; + + @Transactional(readOnly = true) + public List getNewestVideos(String type) { + + List queryResults; + + if (type.equalsIgnoreCase("all")) { + queryResults = tripRecordScheduleVideoRepository.findNewestVideos(HOME_CONTENT_SIZE); + } else if (type.equalsIgnoreCase("domestic")) { + queryResults = tripRecordScheduleVideoRepository.findNewestVideosDomestic(HOME_CONTENT_SIZE); + } else { + queryResults = tripRecordScheduleVideoRepository.findNewestVideosOverseas(HOME_CONTENT_SIZE); + } + + return queryResults.stream() + .map(TripRecordScheduleVideoListItemResponseDto::fromQueryDto) + .toList(); + } + + @Transactional(readOnly = true) + public List getVideosInMemberIds(List memberIds) { + + return tripRecordScheduleVideoRepository + .findInMemberIds(memberIds) + .stream() + .map(TripRecordScheduleVideoListItemResponseDto::fromQueryDto) + .toList(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordService.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordService.java new file mode 100644 index 00000000..8b8a08c8 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordService.java @@ -0,0 +1,203 @@ +package com.haejwo.tripcometrue.domain.triprecord.service; + +import com.haejwo.tripcometrue.domain.store.repository.TripRecordStoreRepository; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordListRequestAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordSearchParamAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.MyTripRecordListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordDetailResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordHotShortsListResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord_schedule_media.TripRecordScheduleVideoDetailDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordViewCount; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordScheduleVideoNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video.TripRecordScheduleVideoRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_viewcount.TripRecordViewCountRepository; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.service.TripRecordViewHistoryService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TripRecordService { + + private final TripRecordRepository tripRecordRepository; + private final TripRecordViewCountRepository tripRecordViewCountRepository; + private final TripRecordViewHistoryService tripRecordViewHistoryService; + private final TripRecordScheduleVideoRepository tripRecordScheduleVideoRepository; + private final TripRecordStoreRepository tripRecordStoreRepository; + + private static final int HOME_CONTENT_SIZE = 5; + + @Transactional + public TripRecordDetailResponseDto findTripRecord(PrincipalDetails principalDetails, Long tripRecordId) { + + TripRecord findTripRecord = findTripRecordById(tripRecordId); + + Long memberId = null; + Boolean isStored = false; + + if(principalDetails != null){ + memberId = principalDetails.getMember().getId(); + } + + if(memberId != findTripRecord.getMember().getId()) { findTripRecord.incrementViewCount(); } + incrementViewCount(findTripRecord); + + if(principalDetails != null) { + tripRecordViewHistoryService.addViewHistory(principalDetails, tripRecordId); + isStored = tripRecordStoreRepository.existsByMemberAndTripRecord(principalDetails.getMember(), findTripRecord); + } + + TripRecordDetailResponseDto responseDto = TripRecordDetailResponseDto.fromEntity(findTripRecord, isStored); + + return responseDto; + } + + @Transactional + public List findTripRecordList( + Pageable pageable, TripRecordListRequestAttribute request + ) { + + List result = tripRecordRepository.finTripRecordWithFilter(pageable, request); + + + return result; + } + + @Transactional(readOnly = true) + public SliceResponseDto findTripRecordList( + TripRecordSearchParamAttribute searchParamAttribute, + Pageable pageable + ) { + return SliceResponseDto.of( + tripRecordRepository.findTripRecordsByFilter(searchParamAttribute, pageable) + .map(tripRecord -> TripRecordListItemResponseDto.fromEntity(tripRecord, null, tripRecord.getMember())) + ); + } + + @Transactional(readOnly = true) + public SliceResponseDto listTripRecordsByHashtag( + String hashTag, Pageable pageable + ) { + return SliceResponseDto.of( + tripRecordRepository.findTripRecordsByHashtag(hashTag, pageable) + .map(tripRecord -> TripRecordListItemResponseDto.fromEntity(tripRecord, null, tripRecord.getMember())) + ); + } + + @Transactional(readOnly = true) + public SliceResponseDto listTripRecordsByExpenseRangeType( + ExpenseRangeType expenseRangeType, Pageable pageable + ) { + return SliceResponseDto.of( + tripRecordRepository.findTripRecordsByExpenseRangeType(expenseRangeType, pageable) + .map(tripRecord -> TripRecordListItemResponseDto.fromEntity(tripRecord, null, tripRecord.getMember())) + ); + } + + @Transactional(readOnly = true) + public List findTopTripRecordList(String type) { + + List tripRecords; + if (type.equalsIgnoreCase("domestic")) { + tripRecords = tripRecordRepository.findTopTripRecordsDomestic(HOME_CONTENT_SIZE); + } else { + tripRecords = tripRecordRepository.findTopTripRecordsOverseas(HOME_CONTENT_SIZE); + } + + return tripRecords + .stream() + .map(tripRecord -> + TripRecordListItemResponseDto.fromEntity( + tripRecord, + tripRecord.getTripRecordSchedules() + .stream() + .map(tripRecordSchedule -> + tripRecordSchedule.getPlace().getCity().getName() + ).collect(Collectors.toSet()), + tripRecord.getMember() + ) + ) + .toList(); + } + + @Transactional(readOnly = true) + public List findTripRecordsWihMemberInMemberIds(List memberIds) { + + return tripRecordRepository + .findTripRecordsWithMemberInMemberIds(memberIds) + .stream() + .map(tripRecord -> TripRecordListItemResponseDto.fromEntity(tripRecord, null, tripRecord.getMember())) + .toList(); + } + + @Transactional + public void incrementViewCount(TripRecord tripRecord) { + + LocalDate today = LocalDate.now(); + + TripRecordViewCount viewCountEntity = tripRecordViewCountRepository.findByTripRecordAndDate(tripRecord, today) + .orElseGet(() -> createNewViewCount(tripRecord, today)); + + viewCountEntity.incrementViewCount(); + tripRecordViewCountRepository.save(viewCountEntity); + + } + + @Transactional(readOnly = true) + public Page getMyTripRecordsList( + PrincipalDetails principalDetails, Pageable pageable) { + Long memberId = principalDetails.getMember().getId(); + Page tripRecords = tripRecordRepository.findByMemberId(memberId, pageable); + + return tripRecords.map(MyTripRecordListResponseDto::fromEntity); + } + + public List findTripRecordHotShortsList(Pageable pageable) { + + List responseDtos = tripRecordRepository.findTripRecordHotShortsList(pageable); + + return responseDtos; + } + + public TripRecordScheduleVideoDetailDto findTripRecordShortsDetail(Long videoId) { + + TripRecordScheduleVideo video = tripRecordScheduleVideoRepository.findById(videoId) + .orElseThrow(TripRecordScheduleVideoNotFoundException::new); + + TripRecordScheduleVideoDetailDto responseDto = TripRecordScheduleVideoDetailDto.fromEntity(video); + + return responseDto; + } + + private TripRecordViewCount createNewViewCount(TripRecord tripRecord, LocalDate today) { + return TripRecordViewCount.builder() + .date(today) + .tripRecord(tripRecord) + .viewCount(0) + .build(); + } + + private TripRecord findTripRecordById(Long tripRecordId) { + TripRecord findTripRecord = tripRecordRepository.findById(tripRecordId) + .orElseThrow(TripRecordNotFoundException::new); + return findTripRecord; + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/controller/TripRecordViewHistoryController.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/controller/TripRecordViewHistoryController.java new file mode 100644 index 00000000..da443b07 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/controller/TripRecordViewHistoryController.java @@ -0,0 +1,36 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.controller; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.dto.response.TripRecordViewHistoryResponseDto; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.service.TripRecordViewHistoryService; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +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.RestController; + +@RestController +@RequestMapping("v1/trip-records") +@RequiredArgsConstructor +public class TripRecordViewHistoryController { + + private final TripRecordViewHistoryService tripRecordViewHistoryService; + + @GetMapping("/view-history") + public ResponseEntity>> getViewHistory( + @AuthenticationPrincipal PrincipalDetails principalDetails, + @PageableDefault(size = 3, sort = "updatedAt", direction = Sort.Direction.DESC) Pageable pageable) { + + Page historyPage = tripRecordViewHistoryService.getViewHistory(principalDetails, pageable); + ResponseDTO> responseBody = ResponseDTO.okWithData(historyPage); + + return ResponseEntity + .status(responseBody.getCode()) + .body(responseBody); + } + } diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/controller/TripRecordViewHistoryControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/controller/TripRecordViewHistoryControllerAdvice.java new file mode 100644 index 00000000..dc5a50e5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/controller/TripRecordViewHistoryControllerAdvice.java @@ -0,0 +1,21 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.controller; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class TripRecordViewHistoryControllerAdvice { + + @ExceptionHandler(TripRecordNotFoundException.class) + public ResponseEntity> tripRecordNotFoundExceptionHandler(TripRecordNotFoundException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/dto/request/TripRecordViewHistoryRequestDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/dto/request/TripRecordViewHistoryRequestDto.java new file mode 100644 index 00000000..57c712b4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/dto/request/TripRecordViewHistoryRequestDto.java @@ -0,0 +1,16 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.dto.request; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordViewHistory; + +public record TripRecordViewHistoryRequestDto( + Long memberId, + Long tripRecordId +) { + public TripRecordViewHistory toEntity(Member member, TripRecord tripRecord) { + return TripRecordViewHistory.builder() + .member(member) + .tripRecord(tripRecord) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/dto/response/TripRecordViewHistoryResponseDto.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/dto/response/TripRecordViewHistoryResponseDto.java new file mode 100644 index 00000000..115fad64 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/dto/response/TripRecordViewHistoryResponseDto.java @@ -0,0 +1,23 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordViewHistory; +import java.time.LocalDateTime; + +public record TripRecordViewHistoryResponseDto( + Long id, + Long memberId, + Long tripRecordId, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH-mm-ss") + LocalDateTime createdAt +) { + public static TripRecordViewHistoryResponseDto fromEntity( + TripRecordViewHistory tripRecordViewHistory) { + return new TripRecordViewHistoryResponseDto( + tripRecordViewHistory.getId(), + tripRecordViewHistory.getMember().getId(), + tripRecordViewHistory.getTripRecord().getId(), + tripRecordViewHistory.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepository.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepository.java new file mode 100644 index 00000000..288f3035 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepository.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.repository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordViewHistory; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TripRecordViewHistoryRepository + extends JpaRepository, TripRecordViewHistoryRepositoryCustom { + + Page findByMember(Member member, Pageable pageable); + + Optional findByMemberIdAndTripRecordId(Long memberId, Long tripRecordId); +} + diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepositoryCustom.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepositoryCustom.java new file mode 100644 index 00000000..17fa96b4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.repository; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordViewHistoryGroupByQueryDto; + +import java.time.LocalDateTime; +import java.util.List; + +public interface TripRecordViewHistoryRepositoryCustom { + + List findTopListMembers(LocalDateTime start, LocalDateTime end, int size); +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepositoryImpl.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepositoryImpl.java new file mode 100644 index 00000000..f6cf3a92 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/repository/TripRecordViewHistoryRepositoryImpl.java @@ -0,0 +1,48 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.repository; + +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordViewHistoryGroupByQueryDto; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.haejwo.tripcometrue.domain.member.entity.QMember.member; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecord.tripRecord; +import static com.haejwo.tripcometrue.domain.triprecord.entity.QTripRecordViewHistory.tripRecordViewHistory; + +@RequiredArgsConstructor +public class TripRecordViewHistoryRepositoryImpl implements TripRecordViewHistoryRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public List findTopListMembers(LocalDateTime start, LocalDateTime end, int size) { + NumberPath aliasTotalCount = Expressions.numberPath(Long.class, "totalCount"); + + return queryFactory.select( + Projections.constructor( + TripRecordViewHistoryGroupByQueryDto.class, + member.id, + member.memberBase.nickname, + member.introduction, + member.profileImage, + tripRecordViewHistory.id.count().as(aliasTotalCount) + ) + ) + .from(tripRecordViewHistory) + .join(tripRecordViewHistory.tripRecord, tripRecord) + .join(tripRecord.member, member) + .where( + tripRecordViewHistory.updatedAt.between(start, end), + member.memberRating.goe(3.0) + ) + .groupBy(member.id) + .orderBy(aliasTotalCount.desc()) + .limit(size) + .fetch(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/service/TripRecordViewHistoryService.java b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/service/TripRecordViewHistoryService.java new file mode 100644 index 00000000..53089fa1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/domain/triprecordViewHistory/service/TripRecordViewHistoryService.java @@ -0,0 +1,58 @@ +package com.haejwo.tripcometrue.domain.triprecordViewHistory.service; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordViewHistory; +import com.haejwo.tripcometrue.domain.triprecord.exception.TripRecordNotFoundException; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.dto.response.TripRecordViewHistoryResponseDto; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.repository.TripRecordViewHistoryRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import jakarta.transaction.Transactional; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TripRecordViewHistoryService { + + private final TripRecordRepository tripRecordRepository; + private final TripRecordViewHistoryRepository tripRecordViewHistoryRepository; + + @Transactional + public void addViewHistory(PrincipalDetails principalDetails, Long tripRecordId) { + TripRecord tripRecord = tripRecordRepository.findById(tripRecordId) + .orElseThrow(TripRecordNotFoundException::new); + + Member member = principalDetails.getMember(); + Long memberId = member.getId(); + + Optional existingHistory = tripRecordViewHistoryRepository + .findByMemberIdAndTripRecordId(memberId, tripRecordId); + + TripRecordViewHistory history; + if (existingHistory.isPresent()) { //ํ•œ๋ฒˆ ๋ดค๋˜ ๊ธฐ๋ก์ธ ๊ฒฝ์šฐ + history = existingHistory.get(); + history.updateUpdatedAt(); + } else { //์ฒ˜์Œ ๋ณด๋Š” ๊ธฐ๋ก์ธ ๊ฒฝ์šฐ + history = TripRecordViewHistory.builder() + .member(member) + .tripRecord(tripRecord) + .build(); + } + + tripRecordViewHistoryRepository.save(history); + } + + @Transactional + public Page getViewHistory(PrincipalDetails principalDetails, Pageable pageable) { + + return tripRecordViewHistoryRepository.findByMember(principalDetails.getMember(), pageable) + .map(TripRecordViewHistoryResponseDto::fromEntity); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/global/config/AppConfig.java b/src/main/java/com/haejwo/tripcometrue/global/config/AppConfig.java index 5525bd2b..90532544 100644 --- a/src/main/java/com/haejwo/tripcometrue/global/config/AppConfig.java +++ b/src/main/java/com/haejwo/tripcometrue/global/config/AppConfig.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -13,6 +14,7 @@ @Configuration public class AppConfig { + @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); @@ -23,6 +25,9 @@ public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); // RestController์—์„œ json ์‘๋‹ต ์‹œ null ๊ฐ’์˜ ํ•„๋“œ๋Š” ์•„์˜ˆ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๋„๋ก ์„ค์ •ํ•˜๋Š” ๋ถ€๋ถ„ objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + // LocalTime, LocalDateTime ๊ณผ ๊ฐ™์€ ์‹œ๊ฐ„๊ด€๋ จ ํด๋ž˜์Šค์˜ ์ง๋ ฌํ™”, ์—ญ์ง๋ ฌํ™” ํฌํ•จํ•œ ํด๋ž˜์Šค ์„ค์ •์„ ์ถ”๊ฐ€ + objectMapper.registerModule(new JavaTimeModule()); + return objectMapper; } } diff --git a/src/main/java/com/haejwo/tripcometrue/global/config/QuerydslConfig.java b/src/main/java/com/haejwo/tripcometrue/global/config/QuerydslConfig.java new file mode 100644 index 00000000..2d93bf33 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/config/QuerydslConfig.java @@ -0,0 +1,20 @@ +package com.haejwo.tripcometrue.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + private EntityManager em; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(em); + } +} + diff --git a/src/main/java/com/haejwo/tripcometrue/global/config/RedisTemplateConfig.java b/src/main/java/com/haejwo/tripcometrue/global/config/RedisTemplateConfig.java new file mode 100644 index 00000000..30f5cc7e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/config/RedisTemplateConfig.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisTemplateConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort)); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + + redisTemplate.setDefaultSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/config/RestTemplateConfig.java b/src/main/java/com/haejwo/tripcometrue/global/config/RestTemplateConfig.java new file mode 100644 index 00000000..e1d7aa10 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/config/RestTemplateConfig.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.global.config; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + CloseableHttpClient httpClient() { + return HttpClientBuilder.create() + .setConnectionManager( + PoolingHttpClientConnectionManagerBuilder.create() + .setMaxConnPerRoute(50) + .setMaxConnTotal(300) + .build() + ).build(); + } + + @Bean + HttpComponentsClientHttpRequestFactory factory(CloseableHttpClient httpClient) { + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setHttpClient(httpClient); + + return factory; + } + + @Bean + RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) { + return new RestTemplate(factory); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/config/S3Config.java b/src/main/java/com/haejwo/tripcometrue/global/config/S3Config.java new file mode 100644 index 00000000..4b09caa4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/config/S3Config.java @@ -0,0 +1,32 @@ +package com.haejwo.tripcometrue.global.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.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder + .standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/config/SchedulingConfig.java b/src/main/java/com/haejwo/tripcometrue/global/config/SchedulingConfig.java new file mode 100644 index 00000000..2d4b033e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/config/SchedulingConfig.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@Configuration +public class SchedulingConfig { +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/enums/Continent.java b/src/main/java/com/haejwo/tripcometrue/global/enums/Continent.java new file mode 100644 index 00000000..b52e9cef --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/enums/Continent.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.global.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum Continent { + + ASIA("์•„์‹œ์•„"), + AFRICA("์•„ํ”„๋ฆฌ์นด"), + AMERICA("์•„๋ฉ”๋ฆฌ์นด"), + EUROPE("์œ ๋Ÿฝ"), + OCEANIA("์˜ค์„ธ์•„๋‹ˆ์•„"), + KOREA("๋Œ€ํ•œ๋ฏผ๊ตญ"); + + private final String description; +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/enums/Country.java b/src/main/java/com/haejwo/tripcometrue/global/enums/Country.java new file mode 100644 index 00000000..c1b64481 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/enums/Country.java @@ -0,0 +1,50 @@ +package com.haejwo.tripcometrue.global.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum Country { + + KOREA("๋Œ€ํ•œ๋ฏผ๊ตญ", Continent.KOREA, + "https://a.cdn-hotels.com/gdcs/production74/d462/9fe21145-585c-4e7f-9373-24ed559ec010.jpg"), + JAPAN("์ผ๋ณธ", Continent.ASIA, + "https://www.state.gov/wp-content/uploads/2019/04/Japan-2107x1406.jpg"), + THAILAND("ํƒœ๊ตญ", Continent.ASIA, + "https://a.cdn-hotels.com/gdcs/production146/d585/aa60115c-bdfc-479f-88ba-5cb0f15a5af8.jpg?impolicy=fcrop&w=800&h=533&q=medium"), + INDONESIA("์ธ๋„๋„ค์‹œ์•„", Continent.ASIA, + "https://media.timeout.com/images/105240189/750/422/image.jpg"), + SINGAPORE("์‹ฑ๊ฐ€ํฌ๋ฅด", Continent.ASIA, + "https://a.cdn-hotels.com/gdcs/production8/d1098/064a4e00-23ee-4137-8ec3-a0d27bca0782.jpg?impolicy=fcrop&w=800&h=533&q=medium"), + + USA("๋ฏธ๊ตญ", Continent.AMERICA, + "https://static.toiimg.com/photo/msid-84475066,width-96,height-65.cms"), + CANADA("์บ๋‚˜๋‹ค", Continent.AMERICA, + "https://a.cdn-hotels.com/gdcs/production159/d204/01ae3fa0-c55c-11e8-9739-0242ac110006.jpg"), + + FRANCE("ํ”„๋ž‘์Šค", Continent.EUROPE, + "https://www.state.gov/wp-content/uploads/2023/07/shutterstock_667548661v2.jpg"), + UNITED_KINGDOM("์˜๊ตญ", Continent.EUROPE, + "https://www.worldatlas.com/r/w1200/upload/c7/28/32/untitled-design-207.jpg"), + ITALIA("์ดํƒˆ๋ฆฌ์•„", Continent.EUROPE, + "https://tourismmedia.italia.it/is/image/mitur/3200x1800_mete_turistiche_hub_autunno-1?wid=1600&hei=900&fit=constrain,1&fmt=webp"), + GERMANY("๋…์ผ", Continent.EUROPE, + "https://www.state.gov/wp-content/uploads/2018/11/Germany-2109x1406.jpg"), + + GUAM("๊ดŒ", Continent.OCEANIA, + "https://a.cdn-hotels.com/gdcs/production49/d1519/6f89ae5d-542c-4fee-b333-a35761fe33d1.jpg"), + NEW_ZEALAND("๋‰ด์งˆ๋žœ๋“œ", Continent.OCEANIA, + "https://media.gq.com/photos/5ba1680236b2d004cdd843cd/16:9/w_2560%2Cc_limit/new-zealand-queenstown-travel-guide-gq.jpg"), + AUSTRALIA("ํ˜ธ์ฃผ", Continent.OCEANIA, + "https://i.natgeofe.com/k/b76526f3-bb84-489d-b229-56bcc08aa915/australia-sydney-opera-house.jpg?w=1084.125&h=611.625"), + + SOUTH_AFRICA("๋‚จ์•„ํ”„๋ฆฌ์นด ๊ณตํ™”๊ตญ", Continent.AFRICA, + "https://media.istockphoto.com/id/620737858/de/foto/kapstadt-und-die-12-apostel-von-oben.jpg?s=612x612&w=0&k=20&c=GiUof-9yNuxdoPx_u1Yc9v8mwlaIFIvLbPFMVpMNMFE="), + EGYPT("์ด์ง‘ํŠธ", Continent.AFRICA, + "https://e0.pxfuel.com/wallpapers/692/206/desktop-wallpaper-egypt-background-cool-for-me-egyptian.jpg"); + + private final String description; + private final Continent continent; + private final String imageUrl; +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/enums/CurrencyUnit.java b/src/main/java/com/haejwo/tripcometrue/global/enums/CurrencyUnit.java new file mode 100644 index 00000000..f1f4fffc --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/enums/CurrencyUnit.java @@ -0,0 +1,38 @@ +package com.haejwo.tripcometrue.global.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum CurrencyUnit { + + AED("์•„๋ž์—๋ฏธ๋ฆฌํŠธ ๋””๋ฅดํ•จ", 1), + AUD("ํ˜ธ์ฃผ ๋‹ฌ๋Ÿฌ", 1), + BHD("๋ฐ”๋ ˆ์ธ ๋””๋‚˜๋ฅด", 1), + BND("๋ธŒ๋ฃจ๋‚˜์ด ๋‹ฌ๋Ÿฌ", 1), + CAD("์บ๋‚˜๋‹ค ๋‹ฌ๋Ÿฌ", 1), + CHF("์Šค์œ„์Šค ํ”„๋ž‘", 1), + CNH("์œ„์•ˆํ™”", 1), + DKK("๋ด๋งˆ์•„ํฌ ํฌ๋กœ๋„ค", 1), + EUR("์œ ๋กœ", 1), + GBP("์˜๊ตญ ํŒŒ์šด๋“œ", 1), + HKD("ํ™์ฝฉ ๋‹ฌ๋Ÿฌ", 1), + IDR("์ธ๋„๋„ค์‹œ์•„ ๋ฃจํ”ผ์•„", 100), + JPY("์ผ๋ณธ ์—”", 100), + KWD("์ฟ ์›จ์ดํŠธ ๋””๋‚˜๋ฅด", 1), + MYR("๋ง๋ ˆ์ด์‹œ์•„ ๋ง๊ธฐํŠธ", 1), + NLG("๋„ค๋œ๋ž€๋“œ ๊ธธ๋”", 1), + NOK("๋…ธ๋ฅด์›จ์ด ํฌ๋กœ๋„ค", 1), + NZD("๋‰ด์งˆ๋žœ๋“œ ๋‹ฌ๋Ÿฌ", 1), + SAR("์‚ฌ์šฐ๋”” ๋ฆฌ์–„", 1), + SEK("์Šค์›จ๋ด ํฌ๋กœ๋‚˜", 1), + SGD("์‹ฑ๊ฐ€ํฌ๋ฅด ๋‹ฌ๋Ÿฌ", 1), + THB("ํƒœ๊ตญ ๋ฐ”ํŠธ", 1), + USD("๋ฏธ๊ตญ ๋‹ฌ๋Ÿฌ", 1), + ZAR("๋‚จ์•„ํ”„๋ผ์นด๊ณตํ™”๊ตญ ๋žœ๋“œ", 1), + EGP("์ด์ง‘ํŠธ ํŒŒ์šด๋“œ", 1); + + private final String currencyName; + private final Integer standard; +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/exception/ErrorCode.java b/src/main/java/com/haejwo/tripcometrue/global/exception/ErrorCode.java index f348e923..f90a0c8f 100644 --- a/src/main/java/com/haejwo/tripcometrue/global/exception/ErrorCode.java +++ b/src/main/java/com/haejwo/tripcometrue/global/exception/ErrorCode.java @@ -11,6 +11,9 @@ @Getter public enum ErrorCode { + // Permission + PERMISSION_DENIED(HttpStatus.FORBIDDEN, "ํ•ด๋‹น ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๊ถŒํ•œ์ด ์—…์Šต๋‹ˆ๋‹ค."), + // EMAIL EMAIL_SENDING_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "์ด๋ฉ”์ผ ์ „์†ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), EMAIL_TEMPLATE_LOAD_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "์ด๋ฉ”์ผ ํ…œํ”Œ๋ฆฟ ๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), @@ -19,10 +22,67 @@ public enum ErrorCode { // USER USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์›์ž…๋‹ˆ๋‹ค."), USER_ALREADY_REGISTERED(HttpStatus.BAD_REQUEST, "์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค."), + USER_INVALID_ACCESS(HttpStatus.BAD_REQUEST, "์ž˜๋ชป๋œ ์œ ์ €์˜ ์ ‘๊ทผ์ž…๋‹ˆ๋‹ค."), // AUTH INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค."), + // myPage + CURRENT_PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + NEW_PASSWORD_SAME_AS_OLD(HttpStatus.BAD_REQUEST, "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•ฉ๋‹ˆ๋‹ค."), + NEW_PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "๋ณ€๊ฒฝ์„ ์œ„ํ•ด ์ž…๋ ฅํ•˜์‹  ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค."), + EMAIL_NOT_MATCH(HttpStatus.BAD_REQUEST, "์ด๋ฉ”์ผ์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + NICKNAME_ALREADY_EXISTS(HttpStatus.BAD_REQUEST,"์ค‘๋ณต๋œ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค."), + INTRODUCTION_TOO_LONG(HttpStatus.BAD_REQUEST, "์†Œ๊ฐœ๋Š” 20์ž ๋‚ด๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”."), + NICKNAME_CHANGE_NOT_AVAILABLE(HttpStatus.BAD_REQUEST, "๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๊ธฐ๊ฐ„(6๊ฐœ์›”)์ด ์ง€๋‚˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."), + + // CITY + CITY_NOT_FOUND(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋„์‹œ์ž…๋‹ˆ๋‹ค."), + EXCHANGE_RATE_API_FAIL(HttpStatus.BAD_REQUEST, "ํ™˜์œจ Open API ํ˜ธ์ถœ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + + // PLACE + PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰์ง€์ž…๋‹ˆ๋‹ค."), + + // TRIP_RECORD + TRIP_RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰ํ›„๊ธฐ์ž…๋‹ˆ๋‹ค."), + EXPENSE_RANGE_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋น„์šฉ๋ฒ”์œ„์ž…๋‹ˆ๋‹ค."), + + //TRIP_RECORD_SCHEDULE_VIDEO + TRIP_RECORD_SCHEDULE_VIDEO_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰์Šค์ผ€์ฅด ์‡ผ์ธ ์ž…๋‹ˆ๋‹ค."), + + // STORE + STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "๋ณด๊ด€ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + STORE_ALREADY_EXIST(HttpStatus.CONFLICT, "๋ณด๊ด€ ๋ฐ์ดํ„ฐ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."), + + // TRIP_PLAN + TRIP_PLAN_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰๊ณ„ํš์ž…๋‹ˆ๋‹ค."), + + // Likes + LIKES_NOT_FOUND(HttpStatus.NOT_FOUND, "์ข‹์•„์š” ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + LIKES_ALREADY_EXIST(HttpStatus.CONFLICT, "์ด๋ฏธ ์ข‹์•„์š”๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."), + + // S3 + FILE_UPLOAD_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "ํŒŒ์ผ ์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + FILE_EMPTY(HttpStatus.BAD_REQUEST, "์ฒจ๋ถ€ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."), + FILE_NOT_EXISTS(HttpStatus.BAD_REQUEST, "์‚ญ์ œํ•  ํŒŒ์ผ์ด ์ €์žฅ ๊ณต๊ฐ„์— ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."), + MAX_SIZE_EXCEEDED(HttpStatus.PAYLOAD_TOO_LARGE, "ํ—ˆ์šฉ ์šฉ๋Ÿ‰์„ ์ดˆ๊ณผํ•œ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค."), + + // PLACE_REVIEW + PLACE_REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค."), + PLACE_REVIEW_ALREADY_EXISTS(HttpStatus.CONFLICT, "์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ๋ฅผ ์ค‘๋ณต ์ž‘์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + PLACE_REVIEW_DELETE_ALL_FAILURE(HttpStatus.BAD_REQUEST, "์š”์ฒญํ•œ ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ๊ฐ€ ๋ชจ๋‘ ์กด์žฌํ•˜์ง€ ์•Š์•„์„œ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + + // TRIP_RECORD_REVIEW + TRIP_RECORD_REVIEW_ALREADY_EXISTS(HttpStatus.CONFLICT, "์ค‘๋ณตํ•ด์„œ ์—ฌํ–‰ ํ›„๊ธฐ ๋ณ„์ ์„ ๋งค๊ธธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + TRIP_RECORD_REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค."), + REGISTERING_DUPLICATE_TRIP_RECORD_REVIEW(HttpStatus.BAD_REQUEST, "๋ณธ๋ฌธ๊ณผ ์ด๋ฏธ์ง€ ๋“ฑ๋ก์€ ํ•œ ๋ฒˆ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."), + CAN_NOT_MODIFYING_WITHOUT_CONTENT(HttpStatus.BAD_REQUEST, "๋ฆฌ๋ทฐ ๋‚ด์šฉ ์ž‘์„ฑ ์ด์ „์— ๋ณธ๋ฌธ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."), + TRIP_RECORD_REVIEW_DELETE_ALL_FAILURE(HttpStatus.BAD_REQUEST, "์š”์ฒญํ•œ ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ๊ฐ€ ๋ชจ๋‘ ์กด์žฌํ•˜์ง€ ์•Š์•„์„œ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."), + + // COMMENT + TRIP_RECORD_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰ ํ›„๊ธฐ ๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค."), + PLACE_REVIEW_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค."), + // 5xx INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "์„œ๋ฒ„ ๋‚ด๋ถ€ ์—๋Ÿฌ"); diff --git a/src/main/java/com/haejwo/tripcometrue/global/exception/GlobalExceptionRestAdvice.java b/src/main/java/com/haejwo/tripcometrue/global/exception/GlobalExceptionRestAdvice.java index 6a81734f..7ed35ccd 100644 --- a/src/main/java/com/haejwo/tripcometrue/global/exception/GlobalExceptionRestAdvice.java +++ b/src/main/java/com/haejwo/tripcometrue/global/exception/GlobalExceptionRestAdvice.java @@ -1,18 +1,24 @@ package com.haejwo.tripcometrue.global.exception; +import com.haejwo.tripcometrue.global.s3.exception.FileMaxSizeExceededException; import com.haejwo.tripcometrue.global.util.ResponseDTO; -import java.util.Map; + +import java.util.List; import java.util.stream.Collectors; + import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; -import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; + +import java.util.Map; +import java.util.stream.Collectors; /** * @author liyusang1 @@ -66,14 +72,24 @@ public ResponseEntity> handleValidationExceptions( BindingResult bindingResult = e.getBindingResult(); - Map fieldErrors = bindingResult.getFieldErrors() + List fieldErrors = bindingResult.getFieldErrors() .stream() - .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .collect(Collectors.toList()); log.error(e.getMessage(), e); + String errorMessage = String.join(", ", fieldErrors); + return ResponseEntity .status(HttpStatus.BAD_REQUEST) - .body(ResponseDTO.errorWithMessage(HttpStatus.BAD_REQUEST, - fieldErrors.values().toString().substring(1,fieldErrors.values().toString().length()-1))); + .body(ResponseDTO.errorWithMessage(HttpStatus.BAD_REQUEST, errorMessage)); + } + + @ExceptionHandler(MaxUploadSizeExceededException.class) + public ResponseEntity> maxUploadSizeExceededExceptionHandler() { + HttpStatus status = HttpStatus.PAYLOAD_TOO_LARGE; + return ResponseEntity + .status(status) + .body(ResponseDTO.error(new FileMaxSizeExceededException().getErrorCode())); } } diff --git a/src/main/java/com/haejwo/tripcometrue/global/exception/PermissionDeniedException.java b/src/main/java/com/haejwo/tripcometrue/global/exception/PermissionDeniedException.java new file mode 100644 index 00000000..debd4b56 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/exception/PermissionDeniedException.java @@ -0,0 +1,11 @@ +package com.haejwo.tripcometrue.global.exception; + + +public class PermissionDeniedException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.PERMISSION_DENIED; + + public PermissionDeniedException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtAuthenticationFilter.java b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtAuthenticationFilter.java new file mode 100644 index 00000000..ba3344a0 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,140 @@ +package com.haejwo.tripcometrue.global.jwt; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.haejwo.tripcometrue.domain.member.dto.request.LoginRequestDto; +import com.haejwo.tripcometrue.domain.member.dto.response.LoginResponseDto; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.stereotype.Component; + +/** + * @author liyusang1 + * @implNote JWT๋ฅผ ์ด์šฉํ•œ ๋กœ๊ทธ์ธ ์ธ์ฆ (Authentication) ์ฝ”๋“œ + */ +@Component +public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final JwtProvider jwtProvider; + MemberRepository memberRepository; + + public JwtAuthenticationFilter( + AuthenticationManager authenticationManager, + JwtProvider jwtProvider, + MemberRepository memberRepository + ) { + super.setAuthenticationManager(authenticationManager); + this.jwtProvider = jwtProvider; + this.memberRepository = memberRepository; + } + + /** + * ๋กœ๊ทธ์ธ ์ธ์ฆ ์‹œ๋„ + */ + @Override + public Authentication attemptAuthentication( + HttpServletRequest request, + HttpServletResponse response + ) throws AuthenticationException { + try { + // ์š”์ฒญ๋œ JSON ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ์ฒด๋กœ ํŒŒ์‹ฑ + ObjectMapper objectMapper = new ObjectMapper(); + LoginRequestDto loginRequest = objectMapper.readValue(request.getInputStream(), + LoginRequestDto.class); + + // ๋กœ๊ทธ์ธํ•  ๋•Œ ์ž…๋ ฅํ•œ email๊ณผ password๋ฅผ ๊ฐ€์ง€๊ณ  authenticationToken๋ฅผ ์ƒ์„ฑ + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginRequest.email(), + loginRequest.password(), + new ArrayList<>(List.of(new SimpleGrantedAuthority("ROLE_USER"))) + ); + + return this.getAuthenticationManager().authenticate(authenticationToken); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * ์ธ์ฆ ์„ฑ๊ณต์‹œ ์ฟ ํ‚ค์— jwtํ† ํฐ์„ ๋‹ด์œผ๋ ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฐ”๊พธ๋ฉด ๋จ + * Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME,token); + * cookie.setMaxAge(JwtProperties.ACCESS_TOKEN_EXPIRATION_TIME / 1000 * 2); + * // setMaxAge๋Š” ์ดˆ๋‹จ์œ„ + * cookie.setSecure(true); + * cookie.setPath("/"); + * response.addCookie(cookie) + * ๋ฐœ๊ธ‰ํ›„ redirect๋กœ ์ด๋™ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ http ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ์š”์ฒญ ์ฝ”๋“œ response.sendRedirect("/"); + */ + @Override + protected void successfulAuthentication( + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + Authentication authResult + ) throws IOException { + Member member = ((PrincipalDetails) authResult.getPrincipal()).getMember(); + String token = jwtProvider.createToken(member); + + LoginResponseDto loginResponseDto = LoginResponseDto.fromEntity(member, token); + ResponseDTO loginResponse = ResponseDTO.okWithData(loginResponseDto); + + sendJsonResponse(response, loginResponse, HttpStatus.OK); + } + + /** + * ์ธ์ฆ์‹คํŒจ + */ + @Override + protected void unsuccessfulAuthentication( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException, ServletException { + String authenticationErrorMessage = getAuthenticationErrorMessage(exception); + + ResponseDTO errorResponse = ResponseDTO.errorWithMessage(HttpStatus.BAD_REQUEST, + authenticationErrorMessage); + sendJsonResponse(response, errorResponse, HttpStatus.BAD_REQUEST); + } + + private void sendJsonResponse(HttpServletResponse response, Object responseData, + HttpStatus httpStatus) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + String jsonResponse = objectMapper.writeValueAsString(responseData); + + response.setStatus(httpStatus.value()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(jsonResponse); + } + + private String getAuthenticationErrorMessage(AuthenticationException exception) { + if (exception instanceof BadCredentialsException) { + return "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ์—๋Ÿฌ"; + } else if (exception instanceof UsernameNotFoundException) { + return "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €"; + } else { + return "์ธ์ฆ ์‹คํŒจ"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtAuthorizationFilter.java b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtAuthorizationFilter.java new file mode 100644 index 00000000..83b3398b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtAuthorizationFilter.java @@ -0,0 +1,80 @@ +package com.haejwo.tripcometrue.global.jwt; + +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.global.springsecurity.PrincipalDetails; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * @author liyusang1 + * @implNote JWT๋ฅผ ์ด์šฉํ•œ ์ธ๊ฐ€ (Authorization) ์ฝ”๋“œ + */ +@Component +public class JwtAuthorizationFilter extends OncePerRequestFilter { + + private final MemberRepository memberRepository; + private final JwtProvider jwtProvider; + + public JwtAuthorizationFilter( + MemberRepository memberRepository, + JwtProvider jwtProvider + ) { + this.memberRepository = memberRepository; + this.jwtProvider = jwtProvider; + } + + /** + * header๊ฐ€ ์•„๋‹Œ cookie์—์„œ ํ† ํฐ์„ ๊ฐ€์ ธ์˜ค๋ ค๊ณ  ํ•˜๋Š” ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฐ”๊พธ๋ฉด ๋œ๋‹ค. + * accessToken = Arrays.stream(request.getCookies()) + * .filter(cookie ->cookie.getName().equals(JwtProperties.COOKIE_NAME)).findFirst().map(Cookie::getValue).orElse(null); + */ + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain + ) throws IOException, ServletException { + //header์—์„œ ๊ฐ€์ ธ์˜ด + List headerValues = Collections.list(request.getHeaders("Authorization")); + String accessToken = headerValues.stream() + .findFirst() + .map(header -> header.replace("Bearer ", "")) + .orElse(null); + + //ํ˜„์žฌ ํ† ํฐ์„ ์‚ฌ์šฉ ํ•˜์—ฌ ์ธ์ฆ์„ ์‹œ๋„ ํ•ฉ๋‹ˆ๋‹ค. + Authentication authentication = getUsernamePasswordAuthenticationToken(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + + chain.doFilter(request, response); + } + + /** + * JWT ํ† ํฐ์œผ๋กœ User๋ฅผ ์ฐพ์•„์„œ UsernamePasswordAuthenticationToken๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + */ + private Authentication getUsernamePasswordAuthenticationToken(String token) { + if (token == null) { + return null; + } + String email = jwtProvider.getEmail(token); + if (email != null) { + return memberRepository.findByMemberBaseEmail(email) + .map(PrincipalDetails::new) + .map(principalDetails -> new UsernamePasswordAuthenticationToken( + principalDetails, // principal + null, // credentials + principalDetails.getAuthorities() + )).orElseThrow(IllegalAccessError::new); + } + return null; // ์œ ์ €๊ฐ€ ์—†์œผ๋ฉด NULL + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtKey.java b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtKey.java new file mode 100644 index 00000000..64b7c9b4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtKey.java @@ -0,0 +1,53 @@ +package com.haejwo.tripcometrue.global.jwt; + +import io.github.cdimascio.dotenv.Dotenv; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Map; +import java.util.Random; +import org.springframework.data.util.Pair; + +/** + * @author liyusang1 + * @implNote JWT Key๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์กฐํšŒํ•˜๋Š” ์ฝ”๋“œ + */ +public class JwtKey { + + private static Dotenv dotenv = Dotenv.load(); + private static final String JWT_SECRET_KEY1 = dotenv.get("JWT_SECRET_KEY1"); + private static final String JWT_SECRET_KEY2 = dotenv.get("JWT_SECRET_KEY2"); + private static final String JWT_SECRET_KEY3 = dotenv.get("JWT_SECRET_KEY3"); + private static final Map SECRET_KEY_SET = Map.of( + "key1", JWT_SECRET_KEY1, + "key2", JWT_SECRET_KEY2, + "key3", JWT_SECRET_KEY3 + ); + private static final String[] KID_SET = SECRET_KEY_SET.keySet().toArray(new String[0]); + private static Random randomIndex = new Random(); + + /** + * SECRET_KEY_SET ์—์„œ ๋žœ๋คํ•œ KEY ๊ฐ€์ ธ์˜ค๊ธฐ + * + * @return kid์™€ key Pair + */ + public static Pair getRandomKey() { + String kid = KID_SET[randomIndex.nextInt(KID_SET.length)]; + String secretKey = SECRET_KEY_SET.get(kid); + return Pair.of(kid, Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))); + } + + /** + * kid๋กœ Key์ฐพ๊ธฐ + * + * @param kid kid + * @return Key + */ + public static Key getKey(String kid) { + String key = SECRET_KEY_SET.getOrDefault(kid, null); + if (key == null) { + return null; + } + return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtProperties.java b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtProperties.java new file mode 100644 index 00000000..61df9e2b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtProperties.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.global.jwt; + + +public class JwtProperties { + + public static final int ACCESS_TOKEN_EXPIRATION_TIME = 1000 * 60 * 60 * 40; // 10๋ถ„ -> 600000 + public static final int REFRESH_TOKEN_EXPIRATION_TIME = 1000 * 60 * 60 * 40; + public static final String COOKIE_NAME = "JWT-AUTHENTICATION"; +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtProvider.java b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtProvider.java new file mode 100644 index 00000000..8ed1c308 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/jwt/JwtProvider.java @@ -0,0 +1,69 @@ +package com.haejwo.tripcometrue.global.jwt; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwts; +import java.security.Key; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + + /** + * @author liyusang1 + * @implNote ํ† ํฐ์—์„œ ์œ ์ € ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ์ฝ”๋“œ + */ + public String getEmail(String token) { + // jwtToken์—์„œ email์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + return Jwts.parserBuilder() + .setSigningKeyResolver(SigningKeyResolver.instance) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + + /** + * member๋กœ ํ† ํฐ ์ƒ์„ฑ HEADER : alg, kid PAYLOAD : sub, iat, exp SIGNATURE : JwtKey.getRandomKey๋กœ ๊ตฌํ•œ + * Secret Key๋กœ HS512 ํ•ด์‹œ + * + * @param member ์œ ์ € + * @return jwt token + */ + public String createToken(Member member) { + Claims claims = Jwts.claims().setSubject(member.getMemberBase().getEmail()); // subject + Date now = new Date(); // ํ˜„์žฌ ์‹œ๊ฐ„ + Pair key = JwtKey.getRandomKey(); + // JWT Token ์ƒ์„ฑ + return Jwts.builder() + .setClaims(claims) // ์ •๋ณด ์ €์žฅ + .setIssuedAt(now) // ํ† ํฐ ๋ฐœํ–‰ ์‹œ๊ฐ„ ์ •๋ณด + .setExpiration( + new Date(now.getTime() + JwtProperties.ACCESS_TOKEN_EXPIRATION_TIME)) // ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • + .setHeaderParam(JwsHeader.KEY_ID, key.getFirst()) // kid + .signWith(key.getSecond()) // signature + .compact(); + } + + public String createRefreshToken(String email) { + Claims claims = Jwts.claims().setSubject(email); // subject + Date now = new Date(); // ํ˜„์žฌ ์‹œ๊ฐ„ + Pair key = JwtKey.getRandomKey(); + // JWT Token ์ƒ์„ฑ + String refreshToken = Jwts.builder() + .setClaims(claims) // ์ •๋ณด ์ €์žฅ + .setIssuedAt(now) // ํ† ํฐ ๋ฐœํ–‰ ์‹œ๊ฐ„ ์ •๋ณด + .setExpiration(new Date( + now.getTime() + JwtProperties.REFRESH_TOKEN_EXPIRATION_TIME)) // ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • + .setHeaderParam(JwsHeader.KEY_ID, key.getFirst()) // kid + .signWith(key.getSecond()) // signature + .compact(); + + return refreshToken; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/jwt/SigningKeyResolver.java b/src/main/java/com/haejwo/tripcometrue/global/jwt/SigningKeyResolver.java new file mode 100644 index 00000000..16a895d5 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/jwt/SigningKeyResolver.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.global.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import java.security.Key; + +/** + * @author liyusang1 + * @implNote JwsHeader๋ฅผ ํ†ตํ•ด Signature ๊ฒ€์ฆ์— ํ•„์š”ํ•œ Key๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ฝ”๋“œ + */ +public class SigningKeyResolver extends SigningKeyResolverAdapter { + + public static SigningKeyResolver instance = new SigningKeyResolver(); + + @Override + public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) { + String kid = jwsHeader.getKeyId(); + if (kid == null) { + return null; + } + return JwtKey.getKey(kid); + } +} \ No newline at end of file diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/controller/S3Controller.java b/src/main/java/com/haejwo/tripcometrue/global/s3/controller/S3Controller.java new file mode 100644 index 00000000..209149f4 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/controller/S3Controller.java @@ -0,0 +1,34 @@ +package com.haejwo.tripcometrue.global.s3.controller; + +import com.haejwo.tripcometrue.global.s3.response.S3UploadResponseDto; +import com.haejwo.tripcometrue.global.s3.service.S3Service; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@RestController +@RequiredArgsConstructor +@Component +public class S3Controller { + + private final S3Service s3Service; + + @PostMapping("/v1/images") + public ResponseEntity> uploadImage( + @RequestPart("file") MultipartFile multipartFile) throws IOException { + ResponseDTO responseDto = ResponseDTO.okWithData(s3Service.saveImage(multipartFile)); + return ResponseEntity.status(responseDto.getCode()).body(responseDto); + } + + @DeleteMapping("/v1/images") + public ResponseEntity deleteImage(@RequestParam("imageUrl") String imageUrl) { + s3Service.removeImage(imageUrl); + ResponseDTO responseDTO = ResponseDTO.ok(); + return ResponseEntity.status(responseDTO.getCode()).body(responseDTO); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/controller/S3ControllerAdvice.java b/src/main/java/com/haejwo/tripcometrue/global/s3/controller/S3ControllerAdvice.java new file mode 100644 index 00000000..bb2d04e9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/controller/S3ControllerAdvice.java @@ -0,0 +1,41 @@ +package com.haejwo.tripcometrue.global.s3.controller; + +import com.haejwo.tripcometrue.global.s3.exception.FileEmptyException; +import com.haejwo.tripcometrue.global.s3.exception.FileNotExistsException; +import com.haejwo.tripcometrue.global.s3.exception.FileUploadFailException; +import com.haejwo.tripcometrue.global.util.ResponseDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class S3ControllerAdvice { + + @ExceptionHandler(FileUploadFailException.class) + public ResponseEntity> fileUploadExceptionHandler(FileUploadFailException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(FileEmptyException.class) + public ResponseEntity> fileEmptyExceptionHandler(FileEmptyException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } + + @ExceptionHandler(FileNotExistsException.class) + public ResponseEntity> fileNotExistsExceptionHandler(FileNotExistsException e) { + HttpStatus status = e.getErrorCode().getHttpStatus(); + + return ResponseEntity + .status(status) + .body(ResponseDTO.errorWithMessage(status, e.getMessage())); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileEmptyException.java b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileEmptyException.java new file mode 100644 index 00000000..a799845e --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileEmptyException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.global.s3.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class FileEmptyException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.FILE_EMPTY; + + public FileEmptyException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileMaxSizeExceededException.java b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileMaxSizeExceededException.java new file mode 100644 index 00000000..00217872 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileMaxSizeExceededException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.global.s3.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class FileMaxSizeExceededException extends ApplicationException { + + private final static ErrorCode ERROR_CODE = ErrorCode.MAX_SIZE_EXCEEDED; + + public FileMaxSizeExceededException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileNotExistsException.java b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileNotExistsException.java new file mode 100644 index 00000000..24d1796b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileNotExistsException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.global.s3.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class FileNotExistsException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.FILE_NOT_EXISTS; + + public FileNotExistsException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileUploadFailException.java b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileUploadFailException.java new file mode 100644 index 00000000..cf1ba783 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/exception/FileUploadFailException.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.global.s3.exception; + +import com.haejwo.tripcometrue.global.exception.ApplicationException; +import com.haejwo.tripcometrue.global.exception.ErrorCode; + +public class FileUploadFailException extends ApplicationException { + + private static final ErrorCode ERROR_CODE = ErrorCode.FILE_UPLOAD_FAIL; + + public FileUploadFailException() { + super(ERROR_CODE); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/response/S3UploadResponseDto.java b/src/main/java/com/haejwo/tripcometrue/global/s3/response/S3UploadResponseDto.java new file mode 100644 index 00000000..9d87a649 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/response/S3UploadResponseDto.java @@ -0,0 +1,9 @@ +package com.haejwo.tripcometrue.global.s3.response; + +public record S3UploadResponseDto( + String imageUrl +) { + public S3UploadResponseDto(String imageUrl) { + this.imageUrl = imageUrl; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/s3/service/S3Service.java b/src/main/java/com/haejwo/tripcometrue/global/s3/service/S3Service.java new file mode 100644 index 00000000..392e39fd --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/s3/service/S3Service.java @@ -0,0 +1,73 @@ +package com.haejwo.tripcometrue.global.s3.service; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.haejwo.tripcometrue.global.s3.exception.FileEmptyException; +import com.haejwo.tripcometrue.global.s3.exception.FileNotExistsException; +import com.haejwo.tripcometrue.global.s3.exception.FileUploadFailException; +import com.haejwo.tripcometrue.global.s3.response.S3UploadResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class S3Service { + private final AmazonS3Client amazonS3; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + public S3UploadResponseDto saveImage(MultipartFile multipartFile) { + validateFileExists(multipartFile); + String filename = generateFilename(multipartFile); + + try (InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject(bucketName, filename, inputStream, getObjectMetadata(multipartFile)); + } catch (IOException e) { + throw new FileUploadFailException(); + } + return new S3UploadResponseDto(amazonS3.getUrl(bucketName, filename).toString()); + } + + private String generateFilename(MultipartFile multipartFile) { + return UUID.randomUUID() + "_" + multipartFile.getOriginalFilename(); + } + + private ObjectMetadata getObjectMetadata(MultipartFile multipartFile) { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(multipartFile.getSize()); + metadata.setContentType(multipartFile.getContentType()); + return metadata; + } + + public void removeImage(String url) { + String filename = getFilename(url); + if (!amazonS3.doesObjectExist(bucketName, filename)) { + throw new FileNotExistsException(); + } + + amazonS3.deleteObject(bucketName, filename); + } + + private String getFilename(String url) { + String encodedName = url.substring(url.lastIndexOf("/") + 1); + try { + return URLDecoder.decode(encodedName, StandardCharsets.UTF_8.toString()); + } catch (Exception e) {} + return ""; + } + + private void validateFileExists(MultipartFile multipartFile) { + if (multipartFile.isEmpty()) { + throw new FileEmptyException(); + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/ApplicationAuditAware.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/ApplicationAuditAware.java new file mode 100644 index 00000000..6c345c4b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/ApplicationAuditAware.java @@ -0,0 +1,28 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import java.util.Optional; +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; + +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(); + } + + Member memberPrincipal = ((PrincipalDetails) authentication.getPrincipal()).getMember(); + return Optional.ofNullable(memberPrincipal.getId()); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/AuthConfig.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/AuthConfig.java new file mode 100644 index 00000000..7bc32a26 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/AuthConfig.java @@ -0,0 +1,61 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +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.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class AuthConfig { + + private final MemberRepository memberRepository; + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public UserDetailsService userDetailsService() { + return this::loadUserByUsername; + } + + @Bean + public AuditorAware auditorAware() { + return new ApplicationAuditAware(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } + + // AppConfig์—์„œ ์ •์˜ํ•œ PasswordEncoder ๋นˆ์„ ์ฐธ์กฐ + @Autowired + private PasswordEncoder passwordEncoder; + + @Bean + public PasswordEncoder passwordEncoder() { + return passwordEncoder; + } + + private PrincipalDetails loadUserByUsername(String email) { + return memberRepository.findByMemberBaseEmail(email) + .map(PrincipalDetails::new) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/GoogleUserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/GoogleUserInfo.java new file mode 100644 index 00000000..b11c4d90 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/GoogleUserInfo.java @@ -0,0 +1,42 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import java.util.Map; + +/** + * @author liyusang1 + * @implNote OAuth2 ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ํ›„ ๋ฐ›์•„์˜จ ๊ฐ’์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค + */ + +public class GoogleUserInfo implements OAuth2UserInfo { + + private Map attributes; + + public GoogleUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getPhoneNumber() { + return null; + } + + @Override + public String getProfileImage() { + return null; + } + + @Override + public String getEmail() { + return (String) attributes.get("email") + "GoogleOAuth2"; + } + + @Override + public String getProvider() { + return "google"; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/KakaoUserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/KakaoUserInfo.java new file mode 100644 index 00000000..b7cf2731 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/KakaoUserInfo.java @@ -0,0 +1,48 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import java.util.Map; + +/** + * @author liyusang1 + * @implNote OAuth2 ์นด์นด์˜ค ๋กœ๊ทธ์ธ ํ›„ ๋ฐ›์•„์˜จ ๊ฐ’์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค + */ + +public class KakaoUserInfo implements OAuth2UserInfo { + + private Map attributes; + + public KakaoUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + // Kakao์˜ ๋‹‰๋„ค์ž„์€ properties ์•ˆ์— ์žˆ์Šต๋‹ˆ๋‹ค. + Map properties = (Map) attributes.get("properties"); + return (String) properties.get("nickname"); + } + + @Override + public String getPhoneNumber() { + return null; + } + + @Override + public String getEmail() { + // Kakao์˜ ์ด๋ฉ”์ผ์€ kakao_account ์•ˆ์— ์žˆ์Šต๋‹ˆ๋‹ค. + Map kakaoAccount = (Map) attributes.get("kakao_account"); + return (String) kakaoAccount.get("email") + "KaKaoOAuth2"; + } + + @Override + public String getProvider() { + return "kakao"; + } + + @Override + public String getProfileImage() { + // Kakao์˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋Š” properties ์•ˆ์— ์žˆ์Šต๋‹ˆ๋‹ค. + Map properties = (Map) attributes.get("properties"); + return (String) properties.get("profile_image"); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/NaverUserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/NaverUserInfo.java new file mode 100644 index 00000000..6ee31953 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/NaverUserInfo.java @@ -0,0 +1,41 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import java.util.Map; + +/** + * @author liyusang1 + * @implNote OAuth2 ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ ํ›„ ๋ฐ›์•„์˜จ ๊ฐ’์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค + */ +public class NaverUserInfo implements OAuth2UserInfo { + + private Map attributes; + + public NaverUserInfo(Map attributes) { + this.attributes = attributes; + } + + @Override + public String getName() { + return (String) attributes.get("name"); + } + + @Override + public String getPhoneNumber() { + return (String) attributes.get("mobile"); + } + + @Override + public String getProfileImage() { + return null; + } + + @Override + public String getEmail() { + return (String) attributes.get("email") + "NaverOAuth2"; + } + + @Override + public String getProvider() { + return "naver"; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2LoginSuccessHandler.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2LoginSuccessHandler.java new file mode 100644 index 00000000..435ddee9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2LoginSuccessHandler.java @@ -0,0 +1,46 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.global.jwt.JwtProvider; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +/** + * @author liyusang1 + * @implNote ํ•ด๋‹น ํด๋ž˜์Šค๋Š” SimpleUrlAuthenticationSuccessHandler๋ฅผ ์ƒ์†๋ฐ›์€ OAuth ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋กœ์ง์„ ์ฒ˜๋ฆฌ ํ•˜๋Š” ํด๋ž˜์Šค + * ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋ฆฌ๋””๋ ‰ํŠธ ํ•˜๊ฒŒ ์„ค์ • ํ–ˆ์Šต๋‹ˆ๋‹ค. + * ํ”„๋ก ํŠธ ๋ฐฐํฌ์‚ฌ์ดํŠธ -> http://localhost:5173/auth/social + * https://tripcometrue.vercel.app + * ์Šคํ”„๋ง ์ฝ”๋“œ ๋‚ด๋กœ ๋ฆฌ๋””๋ ‰ํŠธ ์„ค์ • ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ + * String redirectUrl = "/user/oauth-success?token="+token; + */ +@Component +@RequiredArgsConstructor +public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtProvider jwtProvider; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + + PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); + String token = jwtProvider.createToken(principalDetails.getMember()); + String email = principalDetails.getEmail(); + String name = principalDetails.getUsername(); + + //ํ•œ๊ตญ์–ด ์ธ์ฝ”๋”ฉ ์„ค์ • + String encodedName = URLEncoder.encode(name, StandardCharsets.UTF_8.toString()); + + String redirectUrl = "https://tripcometrue.vercel.app/auth/social?token=" + token + + "&email=" + email + "&name=" + encodedName; + getRedirectStrategy().sendRedirect(request, response, redirectUrl); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2UserInfo.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2UserInfo.java new file mode 100644 index 00000000..bb3329e1 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/OAuth2UserInfo.java @@ -0,0 +1,13 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +/** + * @author liyusang1 + * @implNote OAuth2.0 ์ œ๊ณต์ž๋“ค ๋งˆ๋‹ค ์‘๋‹ต ํ•ด์ฃผ๋Š” ์†์„ฑ ์„ธ๋ถ€ ๊ฐ’์ด ๋‹ฌ๋ผ์„œ ์ƒ์„ฑํ•œ ๊ณตํ†ต interface + */ +public interface OAuth2UserInfo { + String getProvider(); + String getEmail(); + String getName(); + String getPhoneNumber(); + String getProfileImage(); +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalDetails.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalDetails.java new file mode 100644 index 00000000..0e3693f2 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalDetails.java @@ -0,0 +1,89 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +@Getter +@AllArgsConstructor +@RequiredArgsConstructor +public class PrincipalDetails implements UserDetails, OAuth2User { + + @Getter + private Member member; + private String username; + private String password; + private Map attributes; + + //์ผ๋ฐ˜ ๋กœ๊ทธ์ธ + public PrincipalDetails(Member member) { + this.member = member; + } + + //OAuth ๋กœ๊ทธ์ธ + public PrincipalDetails(Member member, Map attributes) { + this.member = member; + } + + @Override + public A getAttribute(String name) { + return OAuth2User.super.getAttribute(name); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(member.getMemberBase().getAuthority())); + } + + @Override + public String getPassword() { + return member.getMemberBase().getPassword(); + } + + @Override + public String getUsername() { + return member.getMemberBase().getNickname(); + } + + @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 String getEmail() { + return member.getMemberBase().getEmail(); + } + + @Override + public String getName() { + return null; + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalOauth2UserService.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalOauth2UserService.java new file mode 100644 index 00000000..1f8f4501 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/PrincipalOauth2UserService.java @@ -0,0 +1,67 @@ +package com.haejwo.tripcometrue.global.springsecurity; + +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +/** + * @author liyusang1 + * @implNote OAuth2 client๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ redirect๋œ ๊ฒฝ๋กœ์˜ ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ํ›„์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ํด๋ž˜์Šค, ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ accesstoken๊ณผ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ + * ๊ฐ™์ด ์ง€๊ธ‰๋ฐ›๊ฒŒ ๋˜๋ฉฐ, ๋ฐœ๊ธ‰๋ฐ›์€ accesstoken ๋ฐ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ฝ”๋“œ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. + * System.out.println("getClientRegistration : " + userRequest.getClientRegistration ()); + * System.out.println("getAccessToken: " + userRequest.getAccessToken()); + * System.out.println("getAttributes: " + super.loadUser(userRequest).getAttributes()) + */ + +@Service +@RequiredArgsConstructor +public class PrincipalOauth2UserService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + + OAuth2User oauth2User = super.loadUser(userRequest); + OAuth2UserInfo oauth2Userinfo = null; + String provider = userRequest.getClientRegistration() + .getRegistrationId(); //google kakao facebook... + + if (provider.equals("google")) { + oauth2Userinfo = new GoogleUserInfo(oauth2User.getAttributes()); + } else if (provider.equals("naver")) { + oauth2Userinfo = new NaverUserInfo((Map) oauth2User.getAttributes().get("response")); + } else if (provider.equals("kakao")) { + oauth2Userinfo = new KakaoUserInfo(oauth2User.getAttributes()); + } + + Optional user = memberRepository.findByMemberBaseEmailAndProvider( + oauth2Userinfo.getEmail(), oauth2Userinfo.getProvider()); + + //์ด๋ฏธ ์†Œ์…œ๋กœ๊ทธ์ธ์„ ํ•œ์ ์ด ์žˆ๋Š”์ง€ ์—†๋Š”์ง€ + if (user.isEmpty()) { + Member newUser = Member.builder() + .email(oauth2Userinfo.getEmail()) + .nickname(oauth2Userinfo.getName()) + .password("OAuth2") //Oauth2๋กœ ๋กœ๊ทธ์ธ์„ ํ•ด์„œ ํŒจ์Šค์›Œ๋“œ๋Š” ์˜๋ฏธ์—†์Œ. + .authority("ROLE_USER") + .provider(provider) + .build(); + if (provider.equals("kakao")) { + newUser.updateProfileImage(oauth2Userinfo.getProfileImage()); + } + + memberRepository.save(newUser); + return new PrincipalDetails(newUser, oauth2User.getAttributes()); + } else { + return new PrincipalDetails(user.get(), oauth2User.getAttributes()); + } + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java index 5de3fc4a..7f094443 100644 --- a/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java +++ b/src/main/java/com/haejwo/tripcometrue/global/springsecurity/SpringSecurityConfig.java @@ -1,16 +1,19 @@ package com.haejwo.tripcometrue.global.springsecurity; +import com.haejwo.tripcometrue.global.jwt.JwtAuthenticationFilter; +import com.haejwo.tripcometrue.global.jwt.JwtAuthorizationFilter; import com.haejwo.tripcometrue.global.util.CustomResponseUtil; import java.util.Arrays; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @@ -19,6 +22,11 @@ @RequiredArgsConstructor public class SpringSecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final JwtAuthorizationFilter jwtAuthorizationFilter; + private final PrincipalOauth2UserService principalOauth2UserService; + private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { @@ -30,6 +38,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, configuration.applyPermitDefaultValues(); configuration.addAllowedOriginPattern(""); configuration.addAllowedOriginPattern("http://localhost:5173"); + configuration.addAllowedOriginPattern("https://tripcometrue.vercel.app"); configuration.setAllowedMethods( Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD")); @@ -48,16 +57,58 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, http.sessionManagement( session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + // jwt filter + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthorizationFilter, BasicAuthenticationFilter.class); + http.authorizeHttpRequests(authz -> authz /* .requestMatchers(new AntPathRequestMatcher("ant matcher")).authenticated() .requestMatchers(new AntPathRequestMatcher("role sample")).hasRole("ADMIN") - .requestMatchers(new AntPathRequestMatcher("role sample", HttpMethod.POST.name())).hasRole("ADMIN") - .requestMatchers(HttpMethod.OPTIONS, "/basket/**").permitAll() // OPTIONS ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ ๊ถŒํ•œ ํ—ˆ์šฉ */ + .requestMatchers(HttpMethod.OPTIONS, "/basket/**").permitAll() // OPTIONS ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•œ ๊ถŒํ•œ ํ—ˆ์šฉ + .requestMatchers(new AntPathRequestMatcher("role sample", HttpMethod.POST.name())).hasRole("ADMIN") */ .requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/v1/member/signup/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/signup")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/test/jwt")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/check-duplicated-email")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/oauth2/info/**")).permitAll() + + .requestMatchers(new AntPathRequestMatcher("/v1/member/check-password")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/change-password")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/profile-image")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/introduction")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/nickname")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/member/details")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/members/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/view-history")).permitAll() + + .requestMatchers(new AntPathRequestMatcher("/v1/cities/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/places/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/images/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/videos/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/trip-places/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/trip-records/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/trip-records-schedules/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/search-schedule-places/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/schedule-place/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/trip-plan/{planId}")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/v1/country-city")).permitAll() + .anyRequest().authenticated()); + /** + * @author liyusang1 + * @implNote ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๊ทธ ์ •๋ณด๋ฅผ ํ† ๋Œ€๋กœ ํšŒ์›๊ฐ€์ž…์„ ์ž๋™์œผ๋กœ ์ง„ํ–‰ + * ์ •๋ณด๊ฐ€ ์ถ”๊ฐ€ ์ ์œผ๋กœ ํ•„์š”ํ•˜๋ฉด ์ถ”๊ฐ€์ ์œผ๋กœ ์š”๊ตฌ ๋ฐ›์•„์•ผํ•จ + * OAuth ์™„๋ฃŒ๊ฐ€ ๋˜๋ฉด ์—‘์„ธ์Šคํ† ํฐ + ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ํ•œ๋ฒˆ์— ๋ฐ›์Œ ๋กœ๊ทธ์ธ ์„ฑ๊ณต์‹œ principalOauth2UserService์—์„œ ์ฒ˜๋ฆฌ ํ›„ + * oAuth2LoginSuccessHandler์—์„œ ๋ฆฌ๋””๋ ‰ํŠธ ์ฒ˜๋ฆฌ + */ + http.oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint( + userInfoEndpoint -> userInfoEndpoint.userService(principalOauth2UserService)) + .successHandler(oAuth2LoginSuccessHandler) + ); + http.exceptionHandling(exceptionHandling -> { exceptionHandling.authenticationEntryPoint( (request, response, authException) -> CustomResponseUtil.fail(response, diff --git a/src/main/java/com/haejwo/tripcometrue/global/util/ResponseDTO.java b/src/main/java/com/haejwo/tripcometrue/global/util/ResponseDTO.java index 282c0063..df334c35 100644 --- a/src/main/java/com/haejwo/tripcometrue/global/util/ResponseDTO.java +++ b/src/main/java/com/haejwo/tripcometrue/global/util/ResponseDTO.java @@ -50,4 +50,20 @@ public static ResponseDTO errorWithMessage(HttpStatus httpStatus, String e .data(null) .build(); } + + public static ResponseDTO successWithData(HttpStatus httpStatus, T data) { + return ResponseDTO.builder() + .code(httpStatus.value()) + .data(data) + .errorMessage(null) + .build(); + } + + public static ResponseDTO errorWithData(HttpStatus httpStatus, T data) { + return ResponseDTO.builder() + .code(httpStatus.value()) + .errorMessage(null) + .data(data) + .build(); + } } diff --git a/src/main/java/com/haejwo/tripcometrue/global/util/SliceResponseDto.java b/src/main/java/com/haejwo/tripcometrue/global/util/SliceResponseDto.java new file mode 100644 index 00000000..c2d55761 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/util/SliceResponseDto.java @@ -0,0 +1,33 @@ +package com.haejwo.tripcometrue.global.util; + +import lombok.Builder; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public record SliceResponseDto( + List content, + SortResponseDto sort, + Integer totalCount, + Integer currentPageNum, + Integer pageSize, + Boolean first, + Boolean last +) { + + @Builder + public SliceResponseDto { + } + + public static SliceResponseDto of(Slice slice) { + return SliceResponseDto.builder() + .content(slice.getContent()) + .sort(SortResponseDto.of(slice.getSort())) + .totalCount(slice.getNumberOfElements()) + .currentPageNum(slice.getNumber()) + .pageSize(slice.getSize()) + .first(slice.isFirst()) + .last(slice.isLast()) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/util/SortResponseDto.java b/src/main/java/com/haejwo/tripcometrue/global/util/SortResponseDto.java new file mode 100644 index 00000000..642595fa --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/util/SortResponseDto.java @@ -0,0 +1,27 @@ +package com.haejwo.tripcometrue.global.util; + +import lombok.Builder; +import org.springframework.data.domain.Sort; + +import java.util.Objects; + +public record SortResponseDto( + Boolean sorted, + String direction, + String orderProperty +) { + + @Builder + public SortResponseDto { + } + + public static SortResponseDto of(Sort sort) { + Sort.Order order = sort.get().findFirst().orElse(null); + + return SortResponseDto.builder() + .sorted(sort.isSorted()) + .direction(Objects.nonNull(order) ? order.getDirection().name() : null) + .orderProperty(Objects.nonNull(order) ? order.getProperty() : null) + .build(); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/validator/HomeTopListQueryTypeValidator.java b/src/main/java/com/haejwo/tripcometrue/global/validator/HomeTopListQueryTypeValidator.java new file mode 100644 index 00000000..d72cd05b --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/validator/HomeTopListQueryTypeValidator.java @@ -0,0 +1,20 @@ +package com.haejwo.tripcometrue.global.validator; + +import com.haejwo.tripcometrue.global.validator.annotation.HomeTopListQueryType; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Objects; + +public class HomeTopListQueryTypeValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + + if (Objects.isNull(value)) { + return false; + } + + return value.equalsIgnoreCase("overseas") || value.equalsIgnoreCase("domestic"); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/validator/HomeVideoListQueryTypeValidator.java b/src/main/java/com/haejwo/tripcometrue/global/validator/HomeVideoListQueryTypeValidator.java new file mode 100644 index 00000000..66981ccd --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/validator/HomeVideoListQueryTypeValidator.java @@ -0,0 +1,21 @@ +package com.haejwo.tripcometrue.global.validator; + +import com.haejwo.tripcometrue.global.validator.annotation.HomeVideoListQueryType; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Objects; + +public class HomeVideoListQueryTypeValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (Objects.isNull(value)) { + return false; + } + + return value.equalsIgnoreCase("all") + || value.equalsIgnoreCase("domestic") + || value.equalsIgnoreCase("overseas"); + } +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/validator/annotation/HomeTopListQueryType.java b/src/main/java/com/haejwo/tripcometrue/global/validator/annotation/HomeTopListQueryType.java new file mode 100644 index 00000000..5c74cbf9 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/validator/annotation/HomeTopListQueryType.java @@ -0,0 +1,19 @@ +package com.haejwo.tripcometrue.global.validator.annotation; + +import com.haejwo.tripcometrue.global.validator.HomeTopListQueryTypeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = HomeTopListQueryTypeValidator.class) +public @interface HomeTopListQueryType { + String message() default "์˜ฌ๋ฐ”๋ฅธ ๊ฒ€์ƒ‰ ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค. [overseas, domestic] ์ค‘ ํ•˜๋‚˜๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/haejwo/tripcometrue/global/validator/annotation/HomeVideoListQueryType.java b/src/main/java/com/haejwo/tripcometrue/global/validator/annotation/HomeVideoListQueryType.java new file mode 100644 index 00000000..073e2d70 --- /dev/null +++ b/src/main/java/com/haejwo/tripcometrue/global/validator/annotation/HomeVideoListQueryType.java @@ -0,0 +1,19 @@ +package com.haejwo.tripcometrue.global.validator.annotation; + +import com.haejwo.tripcometrue.global.validator.HomeVideoListQueryTypeValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = HomeVideoListQueryTypeValidator.class) +public @interface HomeVideoListQueryType { + String message() default "์˜ฌ๋ฐ”๋ฅธ ๊ฒ€์ƒ‰ ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค. [all, overseas, domestic] ์ค‘ ํ•˜๋‚˜๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index d331fcf1..ef191e0e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,7 +7,7 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/tripcometrue?characterEncoding=UTF-8 username: root - password: root + password: ${DB_PASSWORD} jpa: show-sql: true @@ -18,6 +18,61 @@ spring: format_sql: true dialect: org.hibernate.dialect.MySQLDialect + data: + redis: + host: localhost + port: 6379 + + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_ID} + client-secret: ${GOOGLE_KEY} + scope: + - email + - profile + + naver: + client-id: ${NAVER_ID} + client-secret: ${NAVER_KEY} + scope: + - name + - email + client-name: Naver + authorization-grant-type: authorization_code + redirect-uri: http://localhost:8080/login/oauth2/code/naver + + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_KEY} + client-authentication-method: client_secret_post + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + authorization-grant-type: authorization_code + client-name: Kakao + scope: + - profile_nickname + - account_email + - profile_image + + provider: + naver: + authorization-uri: https://nid.naver.com/oauth2.0/authorize + token-uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user-name-attribute: response + 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 + logging: level: "[org.springframework.security]": DEBUG + +cloud: + aws: + s3: + bucket: ${AWS_S3_DEV_BUCKET} diff --git a/src/main/resources/application-docker.yml b/src/main/resources/application-docker.yml deleted file mode 100644 index ec650257..00000000 --- a/src/main/resources/application-docker.yml +++ /dev/null @@ -1,23 +0,0 @@ -spring: - config: - activate: - on-profile: docker - - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: ${DBURL} - username: ${DBUSER} - password: ${DBUSER} - - jpa: - show-sql: true - hibernate: - ddl-auto: update - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.MySQLDialect - -logging: - level: - "[org.springframework.security]": DEBUG \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..abf0710e --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,78 @@ +spring: + config: + activate: + on-profile: prod + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_ID} + client-secret: ${GOOGLE_KEY} + scope: + - email + - profile + + naver: + client-id: ${NAVER_ID} + client-secret: ${NAVER_KEY} + scope: + - name + - email + client-name: Naver + authorization-grant-type: authorization_code + redirect-uri: https://tripcometrue.site/login/oauth2/code/naver + + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_KEY} + client-authentication-method: client_secret_post + redirect-uri: https://tripcometrue.site/login/oauth2/code/kakao + authorization-grant-type: authorization_code + client-name: Kakao + scope: + - profile_nickname + - account_email + - profile_image + + provider: + naver: + authorization-uri: https://nid.naver.com/oauth2.0/authorize + token-uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user-name-attribute: response + 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 + +logging: + level: + "[org.springframework.security]": DEBUG + +cloud: + aws: + s3: + bucket: ${AWS_S3_BUCKET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0f547dcd..373641a2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,8 +5,25 @@ spring: active: dev config: import: optional:file:.env[.properties] + servlet: + multipart: + max-file-size: 5MB + max-request-size: 5MB + jpa: + properties: + hibernate: + default_batch_fetch_size: 200 +cloud: + aws: + credentials: + accessKey: ${AWS_ACCESS_KEY} + secretKey: ${AWS_SECRET_KEY} + region: + static: ${AWS_S3_REGION} - - +exchange-rate: + api: + url: ${EXCHANGE_RATE_API_URL} + key: ${EXCHANGE_RATE_API_KEY} diff --git a/src/test/http/MyPage/changePassword.http b/src/test/http/MyPage/changePassword.http new file mode 100644 index 00000000..1b17e2e7 --- /dev/null +++ b/src/test/http/MyPage/changePassword.http @@ -0,0 +1,22 @@ + +### ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์š”์ฒญ +PATCH http://localhost:8080/v1/member/change-password +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MTIzQG5hdmVyLmNvbSIsImlhdCI6MTcwNTM5MTc2MSwiZXhwIjoxNzA1NTM1NzYxfQ.qj1E_jDYpQI7a8ggUCP4mjuQgADVglgsCTyaQ9J5lF0 + +{ + "confirmPassword": "1234567", + "newPassword": "1234567" +} + + +### ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฒดํฌ ์š”์ฒญ +POST http://localhost:8080/v1/member/check-password +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MTIzQG5hdmVyLmNvbSIsImlhdCI6MTcwNTM5MTc2MSwiZXhwIjoxNzA1NTM1NzYxfQ.qj1E_jDYpQI7a8ggUCP4mjuQgADVglgsCTyaQ9J5lF0 + +{ + "newPassword": "12345678", + "confirmPassword": "12345678" +} + diff --git a/src/test/http/MyPage/change_Info.http b/src/test/http/MyPage/change_Info.http new file mode 100644 index 00000000..ee82233e --- /dev/null +++ b/src/test/http/MyPage/change_Info.http @@ -0,0 +1,31 @@ + +### ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋ฐ์ดํŠธ +PATCH http://localhost:8080/v1/member/profile-image +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MTIzQG5hdmVyLmNvbSIsImlhdCI6MTcwNTE2NzkwMiwiZXhwIjoxNzA1MzExOTAyfQ.OjH2Ex0y_xGq6tIq_qYT8kjLPtBsIDtBxOcCXz5NLRc + +{ + "profile_image": "http://localhost:1234/newimage.jpg" +} + +### ์ž๊ธฐ์†Œ๊ฐœ ์—…๋ฐ์ดํŠธ +PATCH http://localhost:8080/v1/member/introduction +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkxIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MTIzQG5hdmVyLmNvbSIsImlhdCI6MTcwNTM5Mzg2OSwiZXhwIjoxNzA1NTM3ODY5fQ.al9Ft2IAOfSf-UMZtWLw4wZ5z2vsXKpj8x33cp5UIa0 + +{ + "introduction": "์•ˆ๋…•ํ•˜์„ธ์š”, ์ƒˆ๋กœ ๋“ฑ๋กํ•œ ์†Œ๊ฐœ๊ธ€์ž…๋‹ˆ๋‹ค." +} + +### ๋‹‰๋„ค์ž„ ๋ณ€๊ฒฝ +PATCH http://localhost:8080/v1/member/nickname +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkxIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MTIzQG5hdmVyLmNvbSIsImlhdCI6MTcwNTM5Mzg2OSwiZXhwIjoxNzA1NTM3ODY5fQ.al9Ft2IAOfSf-UMZtWLw4wZ5z2vsXKpj8x33cp5UIa0 + +{ + "nickname": "๋ณ€๊ฒฝ๋œ ๋‹‰๋„ค์ž„" +} + +### ํšŒ์› ํƒˆํ‡ด +DELETE http://localhost:8080/v1/member +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MTIzQG5hdmVyLmNvbSIsImlhdCI6MTcwNTE2NzkwMiwiZXhwIjoxNzA1MzExOTAyfQ.OjH2Ex0y_xGq6tIq_qYT8kjLPtBsIDtBxOcCXz5NLRc diff --git a/src/test/http/comment/place_review_comment.http b/src/test/http/comment/place_review_comment.http new file mode 100644 index 00000000..739ac2f3 --- /dev/null +++ b/src/test/http/comment/place_review_comment.http @@ -0,0 +1,20 @@ +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋Œ“๊ธ€ ๋‹ฌ๊ธฐ +POST localhost:8080/v1/places/reviews/1/comments +Content-Type: application/json + +{ + "content": "์ฒซ ๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค ใ…Žใ…Ž" +} + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋Œ€๋Œ“๊ธ€ ๋‹ฌ๊ธฐ +POST localhost:8080/v1/places/reviews/comments/1/reply-comments +Content-Type: application/json + +{ + "content": "๋Œ€๋Œ“๊ธ€ ์ž…๋‹ˆ๋‹ค! ใ…Žใ…Ž" +} + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋Œ“๊ธ€ / ๋Œ€๋Œ“๊ธ€ ์‚ญ์ œ +DELETE localhost:8080/v1/places/reviews/comments/1 \ No newline at end of file diff --git a/src/test/http/comment/trip_record_comment.http b/src/test/http/comment/trip_record_comment.http new file mode 100644 index 00000000..6f5f6d22 --- /dev/null +++ b/src/test/http/comment/trip_record_comment.http @@ -0,0 +1,24 @@ +### ์—ฌํ–‰ ํ›„๊ธฐ ๋Œ“๊ธ€ ๋‹ฌ๊ธฐ +POST localhost:8080/v1/trip-records/{tripRecordId}/comments +Content-Type: application/json + +{ + "content": "์ฒซ ๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค ใ…Žใ…Ž" +} + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋Œ€๋Œ“๊ธ€ ๋‹ฌ๊ธฐ +POST localhost:8080/v1/trip-records/comments/1/reply-comments +Content-Type: application/json + +{ + "content": "๋Œ€๋Œ“๊ธ€ ์ž…๋‹ˆ๋‹ค! ใ…Žใ…Ž" +} + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ์กฐํšŒ +GET localhost:8080/v1/trip-records/{tripRecordId}/comments + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ๋Œ“๊ธ€ / ๋Œ€๋Œ“๊ธ€ ์‚ญ์ œ +DELETE localhost:8080/v1/trip-records/comments/{deleteCommentId} \ No newline at end of file diff --git a/src/test/http/likes/likes.http b/src/test/http/likes/likes.http new file mode 100644 index 00000000..b04df7ee --- /dev/null +++ b/src/test/http/likes/likes.http @@ -0,0 +1,20 @@ +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ์ข‹์•„์š” +POST http://tripcometrue.site/v1/places/reviews/1/likes +Authorization: eyJraWQiOiJrZXkxIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MzMzQG5hdmVyLmNvbSIsImlhdCI6MTcwNjA5NDk1MSwiZXhwIjoxNzA2MjM4OTUxfQ.ojYq1REMoIfIKbP_0O3vcOdWXfASMMZP9d-srehCp7Y +Content-Type: application/json + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ์ข‹์•„์š” ์ทจ์†Œ +DELETE http://localhost:8080/v1/places/reviews/2/likes +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkxIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MzMzQG5hdmVyLmNvbSIsImlhdCI6MTcwNjA5NTQzMywiZXhwIjoxNzA2MjM5NDMzfQ.iA2XkB0wWdxbL_S7BkCEDrTdrxbaW-l6Z2xqhBuOOyw + +### ์—ฌํ–‰ํ›„๊ธฐ ๋ฆฌ๋ทฐ ์ข‹์•„์š” +POST http://localhost:8080/v1/trip-records/reviews/103/likes +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkxIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MzMzQG5hdmVyLmNvbSIsImlhdCI6MTcwNjA5NTQzMywiZXhwIjoxNzA2MjM5NDMzfQ.iA2XkB0wWdxbL_S7BkCEDrTdrxbaW-l6Z2xqhBuOOyw + +### ์—ฌํ–‰ํ›„๊ธฐ ๋ฆฌ๋ทฐ ์ข‹์•„์š” ์ทจ์†Œ +DELETE http://localhost:8080/v1/trip-records/reviews/102/likes +Content-Type: application/json +Authorization: eyJraWQiOiJrZXkxIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MzMzQG5hdmVyLmNvbSIsImlhdCI6MTcwNjA5NTQzMywiZXhwIjoxNzA2MjM5NDMzfQ.iA2XkB0wWdxbL_S7BkCEDrTdrxbaW-l6Z2xqhBuOOyw + diff --git a/src/test/http/member/login.http b/src/test/http/member/login.http new file mode 100644 index 00000000..131fbb01 --- /dev/null +++ b/src/test/http/member/login.http @@ -0,0 +1,19 @@ +### ๋กœ๊ทธ์ธ +POST http://localhost:8080/login +Content-Type: application/json + +{ + "email": "liyusang1@naver.com", + "password": "123456" +} + +### google (Do not run it within an http file) +http://localhost:8080/oauth2/authorization/google +### +http://tripcometrue.site/oauth2/authorization/google + +### naver (Do not run it within an http file) +http://localhost:8080/oauth2/authorization/naver + +### kakao (Do not run it within an http file) +http://localhost:8080/oauth2/authorization/kakao \ No newline at end of file diff --git a/src/test/http/member/signup.http b/src/test/http/member/signup.http index 6ed0b546..b3e3b2a4 100644 --- a/src/test/http/member/signup.http +++ b/src/test/http/member/signup.http @@ -3,7 +3,10 @@ POST http://localhost:8080/v1/member/signup Content-Type: application/json { - "email": "test11@naver.com", - "password": "123456", - "nickname": "testusername" -} \ No newline at end of file + "email": "test123@naver.com", + "password": "123456" +} + +### ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ +GET http://localhost:8080/v1/member/check-duplicated-email?email=test1@naver.com +Content-Type: application/json \ No newline at end of file diff --git a/src/test/http/member/testjwt.http b/src/test/http/member/testjwt.http new file mode 100644 index 00000000..d1f6ab70 --- /dev/null +++ b/src/test/http/member/testjwt.http @@ -0,0 +1,3 @@ +### JWT ํ† ํฐ ํ…Œ์ŠคํŠธ +GET http://localhost:8080/v1/member/test/jwt +Authorization: eyJraWQiOiJrZXkyIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJsaXl1c2FuZzFAbmF2ZXIuY29tIiwiaWF0IjoxNzA0MzkxMTE3LCJleHAiOjE3MDQ1MzUxMTd9.jZrLbQjtoUnHOAq7W4IsMUR2xdBn_9DjjBf2Z_rCxWs \ No newline at end of file diff --git a/src/test/http/myPage/MyPlan, Review, Record.http b/src/test/http/myPage/MyPlan, Review, Record.http new file mode 100644 index 00000000..41c49976 --- /dev/null +++ b/src/test/http/myPage/MyPlan, Review, Record.http @@ -0,0 +1,24 @@ + +### +# MY TripPlan ์กฐํšŒ +GET http://localhost:8080/v1/trip-plan/my-plans-list +Authorization: +Content-Type: application/json + +### +# MY TripRecord ์กฐํšŒ +GET http://localhost:8080/v1/trip-record/my-trip-records-list +Authorization: +Content-Type: application/json + +### +# MY PlaceReview ์กฐํšŒ +#GET http://localhost:8080/v1/place-reviews/my-reviews-list +Authorization: +Content-Type: application/json + +### +# MY TripRecordReview ์กฐํšŒ +#GET http://localhost:8080/v1/trip-record-reviews/my-reviews-list +Authorization: +Content-Type: application/json \ No newline at end of file diff --git a/src/test/http/place/place_get.http b/src/test/http/place/place_get.http new file mode 100644 index 00000000..88050049 --- /dev/null +++ b/src/test/http/place/place_get.http @@ -0,0 +1,2 @@ +### ์—ฌํ–‰์ง€ ์ง€๋„์ •๋ณด +GET http://localhost:8080/v1/places/3/maplist \ No newline at end of file diff --git a/src/test/http/review/place_review/place_review.http b/src/test/http/review/place_review/place_review.http new file mode 100644 index 00000000..edc82d5b --- /dev/null +++ b/src/test/http/review/place_review/place_review.http @@ -0,0 +1,40 @@ +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ์ž‘์„ฑ +POST http://localhost:8080/v1/places/1/reviews +Content-Type: application/json + +{ + "imageUrl": "https://tripcometrue-dev-s3-bucket.s3.ap-northeast-2.amazonaws.com/f42c9d89-b7ac-4565-943f-4cff8fa9747c.png", + "content": "๋„ˆ๋ฌด ๋ฉ‹์ง„ ์—ฌํ–‰์ง€์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ์— ๋˜ ์˜ค๊ณ  ์‹ถ์–ด์š”!!" +} + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋‹จ ๊ฑด ์กฐํšŒ +GET http://localhost:8080/v1/places/reviews/1 + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ (๊ธฐ๋ณธ) +GET http://localhost:8080/v1/places/1/reviews + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ (์ •๋ ฌ ๋ฐ ํ•„ํ„ฐ) +GET http://localhost:8080/v1/places/1/reviews?page=0&size=10&sort=likeCount,desc&onlyImage=true + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ์ˆ˜์ • +PUT http://localhost:8080/v1/places/reviews/1 +Content-Type: application/json + +{ + "imageUrl": "https://tripcometrue-dev-s3-bucket.s3.ap-northeast-2.amazonaws.com/f42c9d89-b7ac-4565-943f-4cff8fa9747c.png", + "content": "๋‚ด์šฉ ์ˆ˜์ •ํ•˜๊ณ  ์‹ถ์–ด์š”!" +} + + +### ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ์‚ญ์ œ +DELETE http://localhost:8080/v1/places/reviews +Content-Type: application/json + +// ๋ณต์ˆ˜ ๊ฐœ์˜ ์—ฌํ–‰์ง€ ๋ฆฌ๋ทฐ ์‚ญ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. +{ + "placeReviewIds": [1, 2, 3] +} \ No newline at end of file diff --git a/src/test/http/review/trip_record_review/trip_record_review.http b/src/test/http/review/trip_record_review/trip_record_review.http new file mode 100644 index 00000000..eb92b41d --- /dev/null +++ b/src/test/http/review/trip_record_review/trip_record_review.http @@ -0,0 +1,54 @@ +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ๋ณ„์  ๋“ฑ๋ก +POST http://localhost:8080/v1/trip-records/1/reviews +Content-Type: application/json + +{ + "ratingScore": 3 +} + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ๋ณธ๋ฌธ ๋ฐ ์ด๋ฏธ์ง€ ๋“ฑ๋ก +PUT http://localhost:8080/v1/trip-records/reviews/1/contents +Content-Type: application/json + +{ + "content": "๋ณธ๋ฌธ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋ฅผ ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”!!", + "imageUrl": "https://tripcometrue-dev-s3-bucket.s3.ap-northeast-2.amazonaws.com/f42c9d89-b7ac-4565-943f-4cff8fa9747c.png" +} + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ์ˆ˜์ • +PUT http://localhost:8080/v1/trip-records/reviews/1 +Content-Type: application/json + +{ + "ratingScore": 4.0, + "imageUrl": "https://tripcometrue-dev-s3-bucket.s3.ap-northeast-2.amazonaws.com/f42c9d89-b7ac-4565-943f-4cff8fa9747c.png", + "content": "์ด ์—ฌํ–‰ ํ›„๊ธฐ ๊ต‰์žฅํžˆ ์œ ์šฉํ•˜๋„ค์š”!!" +} + + +### ์ตœ์‹  ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ 1๊ฑด ์กฐํšŒ +GET http://localhost:8080/v1/trip-records/1/reviews/latest + + +### ํŠน์ • ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ 1๊ฑด ์กฐํšŒ +GET http://localhost:8080/v1/trip-records/reviews/1 + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ (์ •๋ ฌ ๋ฐ ํ•„ํ„ฐ) +GET http://localhost:8080/v1/trip-records/1/reviews?size=10&page=0&sort=ratingScore,desc&sort=likeCount,desc&sort=createdAt,desc + + +### My ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํšŒ +GET http://localhost:8080/v1/trip-records/reviews/my + + +### ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ์‚ญ์ œ +DELETE http://localhost:8080/v1/trip-records/reviews +Content-Type: application/json + +// ๋ณต์ˆ˜ ๊ฐœ์˜ ์—ฌํ–‰ ํ›„๊ธฐ ๋ฆฌ๋ทฐ ์‚ญ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. +{ + "tripRecordReviewIds": [352, 222] +} \ No newline at end of file diff --git a/src/test/http/s3/imagedelete.http b/src/test/http/s3/imagedelete.http new file mode 100644 index 00000000..fb8e664c --- /dev/null +++ b/src/test/http/s3/imagedelete.http @@ -0,0 +1,7 @@ +### ์ด๋ฏธ์ง€ ์‚ญ์ œ (์—ฌ๊ธฐ์„œ ์‹คํ–‰ ์•ˆ๋จ) +DELETE POST localhost:8080/v1/images +Content-Type: application/x-www-form-urlencoded + +{ + "imageUrl": "์‚ญ์ œ ์ด๋ฏธ์ง€ url" +} \ No newline at end of file diff --git a/src/test/http/s3/imageupload.http b/src/test/http/s3/imageupload.http new file mode 100644 index 00000000..3eea1002 --- /dev/null +++ b/src/test/http/s3/imageupload.http @@ -0,0 +1,3 @@ +### ์‚ฌ์ง„ ์—…๋กœ๋“œ (์—ฌ๊ธฐ์„œ ์‹คํ–‰ ์•ˆ๋จ) +POST localhost:8080/v1/images +Content-Type: multipart/form-data \ No newline at end of file diff --git a/src/test/http/store/store.http b/src/test/http/store/store.http new file mode 100644 index 00000000..83f030fe --- /dev/null +++ b/src/test/http/store/store.http @@ -0,0 +1,73 @@ +### ๋„์‹œ ์ €์žฅ +POST http://localhost:8080/v1/cities/stores +Content-Type: application/json +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + +{ + "cityId": "์—ฌ๊ธฐ์— ๋„์‹œ ID ์ž…๋ ฅ" +} + + +### ์—ฌํ–‰์ง€ ์ €์žฅ +POST http://localhost:8080/v1/places/stores +Content-Type: application/json +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + +{ + "placeId": "์—ฌ๊ธฐ์— ์—ฌํ–‰์ง€ ID ์ž…๋ ฅ" +} + + +### ์—ฌํ–‰ ํ›„๊ธฐ ์ €์žฅ +POST http://localhost:8080/v1/trip-records/stores +Content-Type: application/json +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + +{ + "tripRecordId": "์—ฌํ–‰์ง€ ํ›„๊ธฐ ID ์ž…๋ ฅ" +} + + +### ๋„์‹œ ์ €์žฅ ์ทจ์†Œ +DELETE http://localhost:8080/v1/cities/{cityId}/stores +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ์—ฌํ–‰์ง€ ์ €์žฅ ์ทจ์†Œ +DELETE http://localhost:8080/v1/places/{placeId}/stores +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ์—ฌํ–‰ ํ›„๊ธฐ ์ €์žฅ ์ทจ์†Œ +DELETE http://localhost:8080/v1/trip-records/2/stores +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ์ €์žฅ๋œ ๋„์‹œ ๋ชฉ๋ก ์กฐํšŒ +GET http://localhost:8080/v1/cities/stores +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ์ €์žฅ๋œ ์—ฌํ–‰์ง€ ๋ชฉ๋ก ์กฐํšŒ +GET http://localhost:8080/v1/places/stores +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ์ €์žฅ๋œ ์—ฌํ–‰ ํ›„๊ธฐ ๋ชฉ๋ก ์กฐํšŒ +GET http://localhost:8080/v1/trip-records/stores +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ํŠน์ • ๋„์‹œ์˜ ์ €์žฅ๋œ ๊ฐœ์ˆ˜ ์กฐํšŒ +GET http://localhost:8080/v1/cities/{cityId}/stores/count +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ํŠน์ • ์—ฌํ–‰์ง€์˜ ์ €์žฅ๋œ ๊ฐœ์ˆ˜ ์กฐํšŒ +GET http://localhost:8080/v1/places/{placeId}/stores/count +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ + + +### ํŠน์ • ์—ฌํ–‰ ํ›„๊ธฐ์˜ ์ €์žฅ๋œ ๊ฐœ์ˆ˜ ์กฐํšŒ +GET http://localhost:8080/v1/trip-records/{tripRecordId}/stores/count +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ diff --git a/src/test/http/trip_plan/trip_plan.http b/src/test/http/trip_plan/trip_plan.http new file mode 100644 index 00000000..71bda606 --- /dev/null +++ b/src/test/http/trip_plan/trip_plan.http @@ -0,0 +1,70 @@ + +### ์—ฌํ–‰ ๊ณ„ํš ์ž‘์„ฑ +POST http://localhost:8080/v1/trip-plan +Authorization: token +Content-Type: application/json + +{ + "countries": "ํ•œ๊ตญ,์ผ๋ณธ", + "tripStartDay": "2024-01-10", + "tripEndDay": "2024-01-20", + "referencedBy":10, + "tripPlanSchedules": [ + { + "dayNumber": 1, + "orderNumber": 1, + "placeId": 2, + "content": "๋ฐฉ๋ฌธํ•œ ์žฅ์†Œ์— ๋Œ€ํ•œ ๋ฉ”๋ชจ๋‚˜ ์ •๋ณด", + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์Šค์ผ€์ค„ ํƒœ๊ทธ ๋งํฌ1" + }, + { + "dayNumber": 1, + "orderNumber": 2, + "placeId": 2, + "content": "๋‚ด์šฉ", + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์Šค์ผ€์ค„ ํƒœ๊ทธ ๋งํฌ2" + } + ] +} + +### ์—ฌํ–‰ ๊ณ„ํš ์ˆ˜์ • +PUT http://localhost:8080/v1/trip-plan/1 +Authorization: token +Content-Type: application/json + +{ + "countries": "ํ•œ๊ตญ,์ผ๋ณธ,์ค‘๊ตญ", + "tripStartDay": "2024-01-10", + "tripEndDay": "2024-01-20", + "tripPlanSchedules": [ + { + "dayNumber": 1, + "orderNumber": 1, + "placeId": 2, + "content": "์ˆ˜์ • ๋ฐฉ๋ฌธํ•œ ์žฅ์†Œ์— ๋Œ€ํ•œ ๋ฉ”๋ชจ๋‚˜ ์ •๋ณด", + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์ˆ˜์ • ์Šค์ผ€์ค„ ํƒœ๊ทธ ๋งํฌ1" + }, + { + "dayNumber": 1, + "orderNumber": 2, + "placeId": 2, + "content": "์ˆ˜์ • ๋‚ด์šฉ", + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์ˆ˜์ • ์Šค์ผ€์ค„ ํƒœ๊ทธ ๋งํฌ2" + } + ] +} + +### ์—ฌํ–‰ ๊ณ„ํš ์‚ญ์ œ +DELETE http://localhost:8080/v1/trip-plan/1 +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MUBuYXZlci5jb20iLCJpYXQiOjE3MDUxNDE4NzksImV4cCI6MTcwNTI4NTg3OX0.WsPlvC_DJDnh4j3O0x6Di3SRc6FfqjnhRlgMFBOZkaI + +### ์—ฌํ–‰ ๊ณ„ํš ์กฐํšŒ +GET http://localhost:8080/v1/trip-plan/1 + +### ์—ฌํ–‰ ๊ณ„ํš ๋ณต์‚ฌ +GET http://localhost:8080/v1/trip-plan/from-trip-record/1 +Authorization: token \ No newline at end of file diff --git a/src/test/http/trip_record/trip_record.http b/src/test/http/trip_record/trip_record.http new file mode 100644 index 00000000..5612235c --- /dev/null +++ b/src/test/http/trip_record/trip_record.http @@ -0,0 +1,18 @@ +### ์—ฌํ–‰ํ›„๊ธฐ ์กฐํšŒ +GET http://localhost:8080/v1/trip-record/1 + +### ์—ฌํ–‰ํ›„๊ธฐ ์ˆ˜์ • +PUT http://localhost:8080/v1/trip-record/1 +Content-Type: application/json + +{ + "title": "์ˆ˜์ •๋œ ์ œ๋ชฉ01", + "content": "์ˆ˜์ •๋œ ๋‚ด์šฉ01", + "expenseRangeType": "BELOW_100", + "tripStartDay": "2024-12-11", + "tripEndDay": "2024-12-30", + "countries": "๋ฏธ๊ตญ,์ผ๋ณธ" +} + +### ์—ฌํ–‰ํ›„๊ธฐ ์‚ญ์ œ +DELETE http://localhost:8080/v1/trip-record/1 \ No newline at end of file diff --git a/src/test/http/trip_record/trip_record_edit.http b/src/test/http/trip_record/trip_record_edit.http new file mode 100644 index 00000000..23a56c8e --- /dev/null +++ b/src/test/http/trip_record/trip_record_edit.http @@ -0,0 +1,191 @@ +### ์—ฌํ–‰ํ›„๊ธฐ ์ €์žฅ +POST http://localhost:8080/v1/trip-record +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MUBuYXZlci5jb20iLCJpYXQiOjE3MDUxNDE4NzksImV4cCI6MTcwNTI4NTg3OX0.WsPlvC_DJDnh4j3O0x6Di3SRc6FfqjnhRlgMFBOZkaI +Content-Type: application/json + +{ + "tripRecordImages": [ + { + "imageUrl": "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg", + "tagType": "FOOD_TOURISM_LOCATION", + "tagUrl": "https://www.siksinhot.com/P/56310" + }, + { + "imageUrl": "https://www.gangnam.go.kr/upload/editor/2020/12/29/b03f9390-fed9-41fc-874c-81f4d6b200f8.jpg", + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "https://www.skyscanner.co.kr/?&utm_source=google&utm_medium=cpc&utm_campaign=KR-Flights-Search-KO-Generics&utm_term=%ED%95%AD%EA%B3%B5%EA%B6%8C%EC%98%88%EC%95%BD&associateID=SEM_GGF_19370_00080&gclsrc=ds2" + } + ], + "title": "์žฌ๋ฐŒ๋Š” ๊ฐ•๋‚จ -> ์ผ๋ณธ ์—ฌํ–‰", + "content": "์žฌ๋ฐŒ๊ฒŒ ๊ฐ•๋‚จ์„ ๋‹ค๋…€์™”์Šต๋‹ˆ๋‹ค. ๊ฐ•๋‚จ์—ญ์—์„œ ๋ง›์žˆ๋Š” ์Œ์‹์„ ๋จน๊ณ , ์‡ผํ•‘๋„ ํ•˜๊ณ , ์ฆ๊ฑฐ์šด ์‹œ๊ฐ„์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. ๋„์ฟ„ ๋””์ฆˆ๋‹ˆ ๋žœ๋“œ๋„ ๋‹ค๋…€์™”์–ด์š”", + "expenseRangeType": "BELOW_50", + "hashTags": [ + "์—ฐ์ธ๋ผ๋ฆฌ", + "์นœ๊ตฌ๋ผ๋ฆฌ" + ], + "countries": "ํ•œ๊ตญ,์ผ๋ณธ", + "tripStartDay": "2024-01-10", + "tripEndDay": "2024-01-12", + "tripRecordSchedules": [ + { + "dayNumber": 1, + "orderNumber": 1, + "placeId": 6, + "content": "1์ผ์ฐจ ์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฌธ ์žฅ์†Œ ๊ฐ•๋‚จ", + "tripRecordScheduleImages": [ + "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg", + "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg" + ], + "tripRecordScheduleVideos": [ + "https://www.youtube.com/shorts/EjwHIfhr-uE", + "https://www.youtube.com/shorts/EjwHIfhr-uE" + ], + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "https://www.skyscanner.co.kr/?&utm_source=google&utm_medium=cpc&utm_campaign=KR-Flights-Search-KO-Generics&utm_term=%ED%95%AD%EA%B3%B5%EA%B6%8C%EC%98%88%EC%95%BD&associateID=SEM_GGF_19370_00080&gclsrc=ds2" + }, + { + "dayNumber": 1, + "orderNumber": 2, + "placeId": 6, + "content": "1์ผ์ฐจ ๋‘๋ฒˆ์งธ ๋ฐฉ๋ฌธ์žฅ์†Œ ๊ฐ•๋‚จ ๋ง›์ง‘", + "tripRecordScheduleImages": [ + ], + "tripRecordScheduleVideos": [ + ] + }, + { + "dayNumber": 2, + "orderNumber": 1, + "placeId": 7, + "content": "2๋ฒˆ์งธ ๋‚  ์ฒซ์งธ ๋„์ฟ„ ๋””์ฆˆ๋‹ˆ ๋žœ๋“œ ๋ฐฉ๋ฌธ", + "tripRecordScheduleImages": [ + "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg", + "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg" + ], + "tripRecordScheduleVideos": [ + "https://www.youtube.com/shorts/EjwHIfhr-uE", + "https://www.youtube.com/shorts/EjwHIfhr-uE", + "https://www.youtube.com/shorts/EjwHIfhr-uE" + ], + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "https://www.skyscanner.co.kr/?&utm_source=google&utm_medium=cpc&utm_campaign=KR-Flights-Search-KO-Generics&utm_term=%ED%95%AD%EA%B3%B5%EA%B6%8C%EC%98%88%EC%95%BD&associateID=SEM_GGF_19370_00080&gclsrc=ds2" + }, + { + "dayNumber": 2, + "orderNumber": 2, + "placeId": 7, + "content": "2๋ฒˆ์งธ ๋‚  ๋‘˜์งธ ๋„์ฟ„ ๋””์ฆˆ๋‹ˆ ๋žœ๋“œ ๋ฐฉ๋ฌธ", + "tripRecordScheduleImages": [ + "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg", + "https://www.gangnam.go.kr/upload/editor/2022/10/07/af149235-ff84-420d-af5c-e652161de152.jpg" + ], + "tripRecordScheduleVideos": [ + "https://www.youtube.com/shorts/EjwHIfhr-uE", + "https://www.youtube.com/shorts/EjwHIfhr-uE", + "https://www.youtube.com/shorts/EjwHIfhr-uE" + ], + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "https://www.skyscanner.co.kr/?&utm_source=google&utm_medium=cpc&utm_campaign=KR-Flights-Search-KO-Generics&utm_term=%ED%95%AD%EA%B3%B5%EA%B6%8C%EC%98%88%EC%95%BD&associateID=SEM_GGF_19370_00080&gclsrc=ds2" + } + ] +} + +### ์—ฌํ–‰ํ›„๊ธฐ ์‚ญ์ œ +DELETE http://localhost:8080/v1/trip-record/1 +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MUBuYXZlci5jb20iLCJpYXQiOjE3MDUxNDE4NzksImV4cCI6MTcwNTI4NTg3OX0.WsPlvC_DJDnh4j3O0x6Di3SRc6FfqjnhRlgMFBOZkaI + +### ์—ฌํ–‰ํ›„๊ธฐ ์ˆ˜์ • +PUT http://localhost:8080/v1/trip-record/1 +Authorization: eyJraWQiOiJrZXkzIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJ0ZXN0MUBuYXZlci5jb20iLCJpYXQiOjE3MDUxNDE4NzksImV4cCI6MTcwNTI4NTg3OX0.WsPlvC_DJDnh4j3O0x6Di3SRc6FfqjnhRlgMFBOZkaI +Content-Type: application/json + +{ + "tripRecordImages": [ + { + "imageUrl": "์ˆ˜์ • ์ด๋ฏธ์ง€๋งํฌ1", + "tagType": "FOOD_TOURISM_LOCATION", + "tagUrl": "์ˆ˜์ • ์ฃผ์†Œ1" + }, + { + "imageUrl": "์ˆ˜์ • ์ด๋ฏธ์ง€๋งํฌ1", + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์ˆ˜์ • ์ฃผ์†Œ1" + } + ], + "title": "์ˆ˜์ • ํ”ผ๋“œ ์ œ๋ชฉ", + "content": "์ˆ˜์ • ์—ฌํ–‰์˜ ์ „๋ฐ˜์ ์ธ ํ›„๊ธฐ๋‚˜ ๋ฉ”๋ชจ", + "expenseRangeType": "BELOW_50", + "hashTags": [ + "์—ฐ์ธ๋ผ๋ฆฌ", + "์นœ๊ตฌ๋ผ๋ฆฌ", + "ํ˜ผ์ž์—ฌํ–‰", + "๊ฐ€์กฑ์—ฌํ–‰" + ], + "countries": "ํ•œ๊ตญ,์ผ๋ณธ", + "tripStartDay": "2024-01-10", + "tripEndDay": "2024-01-20", + "tripRecordSchedules": [ + { + "dayNumber": 1, + "orderNumber": 1, + "placeId": 2, + "content": "์ˆ˜์ • ๋ฐฉ๋ฌธํ•œ ์žฅ์†Œ์— ๋Œ€ํ•œ ๋ฉ”๋ชจ๋‚˜ ์ •๋ณด", + "tripRecordScheduleImages": [ + "์ˆ˜์ • ์Šค์ผ€์ค„ ์ด๋ฏธ์ง€ ๋งํฌ1", + "์ˆ˜์ • ์Šค์ผ€์ค„ ์ด๋ฏธ์ง€ ๋งํฌ2", + "์ˆ˜์ • ์Šค์ผ€์ค„ ์ด๋ฏธ์ง€ ๋งํฌ2" + ], + "tripRecordScheduleVideos": [ + "์ˆ˜์ • ์Šค์ผ€์ค„ ๋น„๋””์˜ค ๋งํฌ1" + ], + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์ˆ˜์ • ์Šค์ผ€์ค„ ํƒœ๊ทธ ๋งํฌ1" + }, + { + "dayNumber": 1, + "orderNumber": 2, + "placeId": 2, + "content": "์ˆ˜์ • ๋‚ด์šฉ", + "tripRecordScheduleImages": [ + "์ˆ˜์ • ์Šค์ผ€์ค„ ์ด๋ฏธ์ง€ ๋งํฌ3", + "์ˆ˜์ • ์Šค์ผ€์ค„ ์ด๋ฏธ์ง€ ๋งํฌ4" + ], + "tripRecordScheduleVideos": [ + "์ˆ˜์ • ์Šค์ผ€์ค„ ๋น„๋””์˜ค ๋งํฌ4", + "์ˆ˜์ • ์Šค์ผ€์ค„ ๋น„๋””์˜ค ๋งํฌ5", + "์ˆ˜์ • ์Šค์ผ€์ค„ ๋น„๋””์˜ค ๋งํฌ6" + ], + "tagType": "AIRLINE_TICKET_PURCHASE", + "tagUrl": "์ˆ˜์ • ์Šค์ผ€์ค„ ํƒœ๊ทธ ๋งํฌ2" + } + ] +} + +### ์—ฌํ–‰์Šค์ผ€์ค„ ์žฅ์†Œ ๊ฒ€์ƒ‰ํ•˜๊ธฐ +GET http://localhost:8080/v1/search-schedule-places?country=KOREA&city=์„œ์šธ + +### ์—ฌํ–‰์Šค์ผ€์ค„ ์žฅ์†Œ ์ง์ ‘ ๋“ฑ๋กํ•˜๊ธฐ +POST http://localhost:8080/v1/schedule-place +Content-Type: application/json + +{ + "address": "์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 427", + "name": "๊ฐ•๋‚จ์—ญ", + "latitude": 37.498085, + "longitude": 127.027486, + "country": "KOREA", + "cityname": "์„œ์šธ" +} + +### ๊ตญ๊ฐ€ ์กฐํšŒ +GET http://localhost:8080/v1/country-city + +### ๊ตญ๊ฐ€ ์กฐํšŒ +GET http://localhost:8080/v1/country-city?continent=ASIA +### ๊ตญ๊ฐ€ ์กฐํšŒ +GET http://localhost:8080/v1/country-city?continent=AMERICA +### ๊ตญ๊ฐ€ ์กฐํšŒ +GET http://localhost:8080/v1/country-city?continent=EUROPE +### ๊ตญ๊ฐ€ ์กฐํšŒ +GET http://localhost:8080/v1/country-city?continent=OCEANIA + diff --git a/src/test/http/trip_record/view_history.http b/src/test/http/trip_record/view_history.http new file mode 100644 index 00000000..ab531f1f --- /dev/null +++ b/src/test/http/trip_record/view_history.http @@ -0,0 +1,4 @@ +#์ตœ๊ทผ์— ๋ณธ ํ›„๊ธฐ ์กฐํšŒ +GET http://localhost:8080/v1/trip-records/view-history +Content-Type: application/json +Authorization: ํ† ํฐ ๊ฐ’ ์ž…๋ ฅ diff --git a/src/test/java/com/haejwo/tripcometrue/config/AbstractContainersSupport.java b/src/test/java/com/haejwo/tripcometrue/config/AbstractContainersSupport.java new file mode 100644 index 00000000..0faab315 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/config/AbstractContainersSupport.java @@ -0,0 +1,24 @@ +package com.haejwo.tripcometrue.config; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; + +public abstract class AbstractContainersSupport { + + static final String REDIS_IMAGE = "redis:6-alpine"; + static final GenericContainer REDIS_CONTAINER; + + static { + REDIS_CONTAINER = new GenericContainer<>(REDIS_IMAGE) + .withExposedPorts(6379) + .withReuse(true); + REDIS_CONTAINER.start(); + } + + @DynamicPropertySource + public static void overrideProps(DynamicPropertyRegistry registry){ + registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost); + registry.add("spring.data.redis.port", () -> REDIS_CONTAINER.getMappedPort(6379).toString()); + } +} diff --git a/src/test/java/com/haejwo/tripcometrue/config/DatabaseCleanUpAfterEach.java b/src/test/java/com/haejwo/tripcometrue/config/DatabaseCleanUpAfterEach.java new file mode 100644 index 00000000..a18f2854 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/config/DatabaseCleanUpAfterEach.java @@ -0,0 +1,14 @@ +package com.haejwo.tripcometrue.config; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(DatabaseCleanerExtension.class) +public @interface DatabaseCleanUpAfterEach { +} diff --git a/src/test/java/com/haejwo/tripcometrue/config/DatabaseCleanerExtension.java b/src/test/java/com/haejwo/tripcometrue/config/DatabaseCleanerExtension.java new file mode 100644 index 00000000..217ea58d --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/config/DatabaseCleanerExtension.java @@ -0,0 +1,51 @@ +package com.haejwo.tripcometrue.config; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; + +public class DatabaseCleanerExtension implements AfterEachCallback { + + @Override + @Transactional + public void afterEach(ExtensionContext context) throws Exception { + var applicationContext = SpringExtension.getApplicationContext(context); + var jdbcTemplate = applicationContext.getBean(JdbcTemplate.class); + + try (var connection = Objects.requireNonNull(Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection())) { + var rs = connection.getMetaData().getTables(null, null, "%", new String[]{"TABLE"}); + + executeResetTableQuery(jdbcTemplate, rs); + + } catch (Exception exception) { + throw new RuntimeException("database table clean error"); + } + } + + private void executeResetTableQuery(JdbcTemplate jdbcTemplate, ResultSet rs) throws SQLException { + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0"); + + while (rs.next()) { + var tableName = rs.getString("TABLE_NAME"); + jdbcTemplate.execute(createTruncateTableQuery(tableName)); + jdbcTemplate.execute(createResetAutoIncrementQuery(tableName)); + } + + jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1"); + } + + private String createTruncateTableQuery(String tableName) { + return "TRUNCATE TABLE " + tableName; + } + + private String createResetAutoIncrementQuery(String tableName) { + return "ALTER TABLE " + tableName + " AUTO_INCREMENT = 1"; + } + +} diff --git a/src/test/java/com/haejwo/tripcometrue/config/TestQuerydslConfig.java b/src/test/java/com/haejwo/tripcometrue/config/TestQuerydslConfig.java new file mode 100644 index 00000000..c987e4b5 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/config/TestQuerydslConfig.java @@ -0,0 +1,18 @@ +package com.haejwo.tripcometrue.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestQuerydslConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/test/java/com/haejwo/tripcometrue/domain/city/repository/WeatherRepositoryTest.java b/src/test/java/com/haejwo/tripcometrue/domain/city/repository/WeatherRepositoryTest.java new file mode 100644 index 00000000..6af9a7ca --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/city/repository/WeatherRepositoryTest.java @@ -0,0 +1,82 @@ +package com.haejwo.tripcometrue.domain.city.repository; + +import com.haejwo.tripcometrue.config.TestQuerydslConfig; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.city.entity.Weather; +import com.haejwo.tripcometrue.global.enums.Country; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@Import(TestQuerydslConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +class WeatherRepositoryTest { + + @Autowired + private CityRepository cityRepository; + + @Autowired + private WeatherRepository weatherRepository; + + private City city; + + @BeforeEach + void setUp() { + city = cityRepository.save( + City.builder() + .name("๋ฐฉ์ฝ•") + .language("ํƒœ๊ตญ์–ด") + .timeDifference("2์‹œ๊ฐ„ ๋Š๋ฆผ") + .currency(CurrencyUnit.THB) + .visa("visa") + .weatherRecommendation("11~12์›”์ด ์—ฌํ–‰ํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ๊ธฐ์ž…๋‹ˆ๋‹ค.") + .weatherDescription("๋ฐฉ์ฝ• ๋‚ ์”จ ์„ค๋ช…") + .voltage("220V") + .country(Country.THAILAND) + .build() + ); + + for (int i = 1; i <= 12; i++) { + weatherRepository.save( + Weather.builder() + .city(city) + .month(i) + .maxAvgTemp("32.2") + .minAvgTemp("30.1") + .build() + ); + } + } + + @Test + void findAllByCityAndMonthBetweenOrderByMonthAsc() { + // given + List monthList = List.of(10, 11, 12, 1); + + // when + List result = weatherRepository.findAllByCityAndMonthInOrderByMonthAsc(city, monthList); + + // then + assertThat(result).hasSize(4); + assertThat(result.get(0).getMonth()).isEqualTo(1); + assertThat(result.get(3).getMonth()).isEqualTo(12); + } + + @AfterEach + void cleanUp() { + weatherRepository.deleteAll(); + cityRepository.deleteAll(); + } +} diff --git a/src/test/java/com/haejwo/tripcometrue/domain/city/service/CityInfoReadServiceTest.java b/src/test/java/com/haejwo/tripcometrue/domain/city/service/CityInfoReadServiceTest.java new file mode 100644 index 00000000..1b1f35c0 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/city/service/CityInfoReadServiceTest.java @@ -0,0 +1,131 @@ +package com.haejwo.tripcometrue.domain.city.service; + +import com.haejwo.tripcometrue.config.AbstractContainersSupport; +import com.haejwo.tripcometrue.config.DatabaseCleanUpAfterEach; +import com.haejwo.tripcometrue.domain.city.dto.response.CityInfoResponseDto; +import com.haejwo.tripcometrue.domain.city.dto.response.WeatherResponseDto; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.city.entity.Weather; +import com.haejwo.tripcometrue.domain.city.exception.CityNotFoundException; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.city.repository.WeatherRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.global.enums.Country; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Slf4j +@DatabaseCleanUpAfterEach +@SpringBootTest +class CityInfoReadServiceTest extends AbstractContainersSupport { + + @Autowired + private CityInfoReadService cityInfoReadService; + + @Autowired + private CityRepository cityRepository; + + @Autowired + private PlaceRepository placeRepository; + + @Autowired + private WeatherRepository weatherRepository; + + private City city; + + @BeforeEach + void setUp() { + city = cityRepository.save( + City.builder() + .name("๋ฐฉ์ฝ•") + .language("ํƒœ๊ตญ์–ด") + .timeDifference("2์‹œ๊ฐ„ ๋Š๋ฆผ") + .currency(CurrencyUnit.THB) + .visa("visa") + .weatherRecommendation("11~12์›”์ด ์—ฌํ–‰ํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ๊ธฐ์ž…๋‹ˆ๋‹ค.") + .weatherDescription("๋ฐฉ์ฝ• ๋‚ ์”จ ์„ค๋ช…") + .voltage("220V") + .country(Country.THAILAND) + .build() + ); + + for (int i = 1; i <= 15; i++) { + placeRepository.save( + Place.builder() + .city(city) + .address("์—ฌํ–‰์ง€" + i + " ์ฃผ์†Œ") + .name("๋ฐฉ์ฝ• ์—ฌํ–‰์ง€" + i) + .description("์—ฌํ–‰์ง€ ์„ค๋ช…") + .storedCount(100 + i) + .build() + ); + } + + for (int i = 1; i <= 12; i++) { + weatherRepository.save( + Weather.builder() + .city(city) + .month(i) + .maxAvgTemp("32.2") + .minAvgTemp("30.1") + .build() + ); + } + } + + @Test + void getCityInfo() { + // given + long cityId = city.getId(); + + // when + CityInfoResponseDto cityInfo = cityInfoReadService.getCityInfo(cityId); + + // then + assertThat(cityInfo.name()).isEqualTo("๋ฐฉ์ฝ•"); + assertThat(cityInfo.language()).isEqualTo("ํƒœ๊ตญ์–ด"); + assertThat(cityInfo.curUnit()).isEqualTo(CurrencyUnit.THB.name()); + } + + @Test + void getWeatherInfo() { + // given + long cityId = city.getId(); + + // when + List weatherInfos = cityInfoReadService.getWeatherInfo(cityId); + + // then + assertThat(weatherInfos).hasSize(4); + } + + @Test + void getCityInfo_cityNotFoundException() { + // given + long cityId = 10000; + + // when & then + assertThatThrownBy(() -> cityInfoReadService.getCityInfo(cityId)) + .isInstanceOf(CityNotFoundException.class); + } + + @Test + void getWeatherInfo_cityNotFoundException() { + // given + long cityId = 10000; + + // when & then + assertThatThrownBy(() -> cityInfoReadService.getWeatherInfo(cityId)) + .isInstanceOf(CityNotFoundException.class); + } +} diff --git a/src/test/java/com/haejwo/tripcometrue/domain/member/facade/MemberReadSearchFacadeTest.java b/src/test/java/com/haejwo/tripcometrue/domain/member/facade/MemberReadSearchFacadeTest.java new file mode 100644 index 00000000..079780f1 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/member/facade/MemberReadSearchFacadeTest.java @@ -0,0 +1,89 @@ +package com.haejwo.tripcometrue.domain.member.facade; + +import com.haejwo.tripcometrue.config.AbstractContainersSupport; +import com.haejwo.tripcometrue.config.DatabaseCleanUpAfterEach; +import com.haejwo.tripcometrue.domain.member.dto.response.MemberCreatorInfoResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.MemberDetailListItemResponseDto; +import com.haejwo.tripcometrue.domain.member.dto.response.MemberSearchResultWithContentResponseDto; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.jdbc.Sql; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@DatabaseCleanUpAfterEach +@Sql(scripts = "classpath:sql/test-data-insert.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +@SpringBootTest +class MemberReadSearchFacadeTest extends AbstractContainersSupport { + + @Autowired + private MemberReadSearchFacade memberReadSearchFacade; + + @Test + void searchByNicknameResultWithContent() { + // given + String query = "์น˜ ์•™๋งˆ์ด"; + + // when + MemberSearchResultWithContentResponseDto result = memberReadSearchFacade.searchByNicknameResultWithContent(query); + + // then + assertThat(result.members()).hasSize(2); + assertThat(result.tripRecords()).hasSize(3); + assertThat(result.videos()).hasSize(5); + } + + @Test + void searchByNicknamePagination() { + // given + String query = "์น˜ ์•™๋งˆ์ด"; + int pageNum = 0; + int pageSize = 1; + PageRequest pageRequest = PageRequest.of(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "memberRating")); + + // when + SliceResponseDto result = memberReadSearchFacade.searchByNicknamePagination(query, pageRequest); + + // then + assertThat(result.currentPageNum()).isEqualTo(pageNum); + assertThat(result.totalCount()).isEqualTo(pageSize); + assertThat(result.last()).isFalse(); + } + + @Test + void searchByNicknamePagination_emptyResult() { + // given + String query = "์—†์Šต๋‹ˆ๋‹ค."; + int pageNum = 0; + int pageSize = 1; + PageRequest pageRequest = PageRequest.of(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "memberRating")); + + // when + SliceResponseDto result = memberReadSearchFacade.searchByNicknamePagination(query, pageRequest); + + // then + assertThat(result.currentPageNum()).isEqualTo(pageNum); + assertThat(result.totalCount()).isEqualTo(0); + assertThat(result.last()).isTrue(); + } + + @Test + void getCreatorInfo() { + // given + Long memberId = 1L; + + // when + MemberCreatorInfoResponseDto result = memberReadSearchFacade.getCreatorInfo(memberId); + + // then + assertThat(result.memberDetailInfo().memberId()).isEqualTo(memberId); + assertThat(result.tripRecords().size()).isEqualTo(result.memberDetailInfo().tripRecordTotal()); + assertThat(result.videos().size()).isEqualTo(result.memberDetailInfo().videoTotal()); + } +} \ No newline at end of file diff --git a/src/test/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryTest.java b/src/test/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryTest.java new file mode 100644 index 00000000..d9d26d30 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/place/repositroy/PlaceRepositoryTest.java @@ -0,0 +1,80 @@ +package com.haejwo.tripcometrue.domain.place.repositroy; + +import com.haejwo.tripcometrue.config.TestQuerydslConfig; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +import static org.assertj.core.api.Assertions.*; + +@Slf4j +@Sql(scripts = "classpath:sql/test-data-insert.sql") +@ActiveProfiles("test") +@Import(TestQuerydslConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +class PlaceRepositoryTest { + + @Autowired + private PlaceRepository placeRepository; + + @Test + void findPlacesByCityIdAndPlaceName() { + // given + Long cityId = 1L; + String placeName = "์—ฌ ํ–‰"; + int pageSize = 2; + PageRequest pageRequest = PageRequest.of(0, pageSize); + + // when + Slice result = placeRepository.findPlacesByCityIdAndPlaceName(cityId, placeName, pageRequest); + + // then + assertThat(result.getNumberOfElements()).isEqualTo(pageSize); + assertThat(result.isFirst()).isTrue(); + assertThat(result.isLast()).isTrue(); + } + + @Test + void findPlacesByCityIdAndPlaceName_nullPlaceName() { + // given + Long cityId = 1L; + String placeName = null; + int pageSize = 2; + PageRequest pageRequest = PageRequest.of(0, pageSize); + + // when + Slice result = placeRepository.findPlacesByCityIdAndPlaceName(cityId, placeName, pageRequest); + + // then + assertThat(result.getNumberOfElements()).isEqualTo(pageSize); + assertThat(result.isFirst()).isTrue(); + assertThat(result.isLast()).isTrue(); + } + + @Test + void findPlacesByCityIdAndPlaceName_blankPlaceName() { + // given + Long cityId = 1L; + String placeName = " "; + int pageSize = 2; + PageRequest pageRequest = PageRequest.of(0, pageSize); + + // when + Slice result = placeRepository.findPlacesByCityIdAndPlaceName(cityId, placeName, pageRequest); + + // then + assertThat(result.getNumberOfElements()).isEqualTo(pageSize); + assertThat(result.isFirst()).isTrue(); + assertThat(result.isLast()).isTrue(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryTest.java b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryTest.java new file mode 100644 index 00000000..89e6a780 --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_image/TripRecordScheduleImageRepositoryTest.java @@ -0,0 +1,366 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_image; + +import com.haejwo.tripcometrue.config.TestQuerydslConfig; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleImageWithPlaceIdQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleImage; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule.TripRecordScheduleRepository; +import com.haejwo.tripcometrue.global.enums.Country; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@ActiveProfiles("test") +@Import(TestQuerydslConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +class TripRecordScheduleImageRepositoryTest { + + @Autowired + private TripRecordScheduleImageRepository tripRecordScheduleImageRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CityRepository cityRepository; + + @Autowired + private PlaceRepository placeRepository; + + @Autowired + private TripRecordRepository tripRecordRepository; + + @Autowired + private TripRecordScheduleRepository tripRecordScheduleRepository; + + private List cities; + private List places; + + @BeforeEach + void setUp() { + + cities = new ArrayList<>(); + cities.add( + cityRepository.save( + City.builder() + .name("๋ฐฉ์ฝ•") + .language("ํƒœ๊ตญ์–ด") + .timeDifference("2์‹œ๊ฐ„ ๋Š๋ฆผ") + .currency(CurrencyUnit.THB) + .visa("visa") + .weatherRecommendation("11~12์›”์ด ์—ฌํ–‰ํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ๊ธฐ์ž…๋‹ˆ๋‹ค.") + .weatherDescription("๋ฐฉ์ฝ• ๋‚ ์”จ ์„ค๋ช…") + .voltage("220V") + .country(Country.THAILAND) + .build() + ) + ); + + cities.add( + cityRepository.save( + City.builder() + .name("์˜ค์‚ฌ์นด") + .language("์ผ๋ณธ์–ด") + .timeDifference("1์‹œ๊ฐ„ ๋Š๋ฆผ") + .currency(CurrencyUnit.JPY) + .visa("visa") + .weatherRecommendation("11~12์›”์ด ์—ฌํ–‰ํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ๊ธฐ์ž…๋‹ˆ๋‹ค.") + .weatherDescription("์ผ๋ณธ์–ด ๋‚ ์”จ ์„ค๋ช…") + .voltage("220V") + .country(Country.JAPAN) + .build() + ) + ); + + places = new ArrayList<>(); + + places.add( + placeRepository.save( + Place.builder() + .name("๋ฐฉ์ฝ• ์™•๊ถ") + .address("์—ฌํ–‰์ง€ ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€ ์„ค๋ช…") + .storedCount(100) + .city(cities.get(0)) + .build() + ) + ); + + places.add( + placeRepository.save( + Place.builder() + .name("์กฐ๋“œ ํŽ˜์–ด") + .address("์—ฌํ–‰์ง€ ์ฃผ์†Œ2") + .description("์—ฌํ–‰์ง€ ์„ค๋ช…2") + .storedCount(150) + .city(cities.get(0)) + .build() + ) + ); + + places.add( + placeRepository.save( + Place.builder() + .name("์—ฌํ–‰์ง€3") + .address("์—ฌํ–‰์ง€3 ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€3 ์„ค๋ช…") + .storedCount(115) + .city(cities.get(1)) + .build() + ) + ); + + places.add( + placeRepository.save( + Place.builder() + .name("์—ฌํ–‰์ง€3") + .address("์—ฌํ–‰์ง€3 ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€3 ์„ค๋ช…") + .storedCount(200) + .city(cities.get(1)) + .build() + ) + ); + + TripRecord tripRecord1 = tripRecordRepository.save( + TripRecord.builder() + .averageRating(4.0) + .title("์—ฌํ–‰ ํ›„๊ธฐ ์ œ๋ชฉ") + .content("์—ฌํ–‰ ํ›„๊ธฐ") + .countries("ํ”„๋ž‘์Šค,์˜๊ตญ") + .storeCount(100) + .expenseRangeType(ExpenseRangeType.ABOVE_300) + .member(memberRepository.save( + Member.builder() + .authority("ROLE_USER") + .email("member@email.com") + .password("password") + .nickname("member") + .build() + )) + .build() + ); + + TripRecord tripRecord2 = tripRecordRepository.save( + TripRecord.builder() + .averageRating(4.0) + .title("์—ฌํ–‰ ํ›„๊ธฐ ์ œ๋ชฉ") + .content("์—ฌํ–‰ ํ›„๊ธฐ") + .countries("์ผ๋ณธ") + .storeCount(200) + .expenseRangeType(ExpenseRangeType.BELOW_100) + .member(memberRepository.save( + Member.builder() + .authority("ROLE_USER") + .email("member@email.com") + .password("password") + .nickname("member") + .build() + )) + .build() + ); + + TripRecord tripRecord3 = tripRecordRepository.save( + TripRecord.builder() + .averageRating(4.0) + .title("์—ฌํ–‰ ํ›„๊ธฐ ์ œ๋ชฉ") + .content("์—ฌํ–‰ ํ›„๊ธฐ") + .countries("ํƒœ๊ตญ") + .storeCount(150) + .expenseRangeType(ExpenseRangeType.BELOW_100) + .member(memberRepository.save( + Member.builder() + .authority("ROLE_USER") + .email("member@email.com") + .password("password") + .nickname("member") + .build() + )) + .build() + ); + + TripRecordSchedule schedule1 = tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord1) + .place(places.get(0)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ); + + TripRecordSchedule schedule2 = tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord1) + .place(places.get(1)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ); + + TripRecordSchedule schedule3 = tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord2) + .place(places.get(2)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ); + + TripRecordSchedule schedule4 = tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord2) + .place(places.get(3)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ); + + TripRecordSchedule schedule5 = tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord3) + .place(places.get(0)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ); + + TripRecordSchedule schedule6 = tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord3) + .place(places.get(1)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule1) + .imageUrl("imageUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule2) + .imageUrl("imageUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule2) + .imageUrl("imageUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule3) + .imageUrl("imageUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule4) + .imageUrl("imageUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule5) + .imageUrl("imageUrl") + .build() + ); + + tripRecordScheduleImageRepository.save( + TripRecordScheduleImage.builder() + .tripRecordSchedule(schedule6) + .imageUrl("imageUrl") + .build() + ); + } + + @Test + void findByCityId() { + //given + Long cityId = cities.get(0).getId(); + int size = 3; + PageRequest pageRequest = PageRequest.of(0, size, Sort.by(Sort.Direction.DESC, "storedCount")); + + //when + Slice result = tripRecordScheduleImageRepository.findByCityId(cityId, pageRequest); + + //then + assertThat(result.getNumberOfElements()).isEqualTo(size); + assertThat(result.getContent().get(1).getTripRecordSchedule().getTripRecord().getStoreCount()) + .isGreaterThanOrEqualTo(result.getContent().get(0).getTripRecordSchedule().getTripRecord().getStoreCount()); + } + + + @Test + void findByCityIdOrderByCreatedAtDescLimitSize() { + //given + Long cityId = cities.get(0).getId(); + int size = 2; + + //when + List result = tripRecordScheduleImageRepository.findByCityIdOrderByCreatedAtDescLimitSize(cityId, size); + + //then + assertThat(result).hasSize(size); + } + + + @Test + void findInPlaceIdsOrderByCreatedAtDesc() { + // given + List placeIds = places.stream().map(Place::getId).toList(); + + // when + List result = tripRecordScheduleImageRepository.findInPlaceIdsOrderByCreatedAtDesc(placeIds); + + // then + assertThat(result).hasSize(7); + } +} \ No newline at end of file diff --git a/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryTest.java b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryTest.java new file mode 100644 index 00000000..7ea8726e --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_schedule_video/TripRecordScheduleVideoRepositoryTest.java @@ -0,0 +1,330 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule_video; + +import com.haejwo.tripcometrue.config.TestQuerydslConfig; +import com.haejwo.tripcometrue.domain.city.entity.City; +import com.haejwo.tripcometrue.global.enums.CurrencyUnit; +import com.haejwo.tripcometrue.domain.city.repository.CityRepository; +import com.haejwo.tripcometrue.domain.member.entity.Member; +import com.haejwo.tripcometrue.domain.member.repository.MemberRepository; +import com.haejwo.tripcometrue.domain.place.entity.Place; +import com.haejwo.tripcometrue.domain.place.repositroy.PlaceRepository; +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordScheduleVideoQueryDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecord; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordSchedule; +import com.haejwo.tripcometrue.domain.triprecord.entity.TripRecordScheduleVideo; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExternalLinkTagType; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord.TripRecordRepository; +import com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_schedule.TripRecordScheduleRepository; +import com.haejwo.tripcometrue.global.enums.Country; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@ActiveProfiles("test") +@Import(TestQuerydslConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +class TripRecordScheduleVideoRepositoryTest { + + @Autowired + private TripRecordScheduleVideoRepository tripRecordScheduleVideoRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CityRepository cityRepository; + + @Autowired + private PlaceRepository placeRepository; + + @Autowired + private TripRecordRepository tripRecordRepository; + + @Autowired + private TripRecordScheduleRepository tripRecordScheduleRepository; + + private List cities; + private List places; + + @BeforeEach + void setUp() { + + cities = new ArrayList<>(); + cities.add( + cityRepository.save( + City.builder() + .name("๋ฐฉ์ฝ•") + .language("ํƒœ๊ตญ์–ด") + .timeDifference("2์‹œ๊ฐ„ ๋Š๋ฆผ") + .currency(CurrencyUnit.THB) + .visa("visa") + .weatherRecommendation("11~12์›”์ด ์—ฌํ–‰ํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ๊ธฐ์ž…๋‹ˆ๋‹ค.") + .weatherDescription("๋ฐฉ์ฝ• ๋‚ ์”จ ์„ค๋ช…") + .voltage("220V") + .country(Country.THAILAND) + .build() + ) + ); + + cities.add( + cityRepository.save( + City.builder() + .name("๋ถ€์‚ฐ") + .language("ํ•œ๊ตญ์–ด") + .timeDifference("์‹œ์ฐจ์—†์Œ") + .weatherRecommendation("11~12์›”์ด ์—ฌํ–‰ํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์‹œ๊ธฐ์ž…๋‹ˆ๋‹ค.") + .weatherDescription("์ผ๋ณธ์–ด ๋‚ ์”จ ์„ค๋ช…") + .country(Country.KOREA) + .build() + ) + ); + + places = new ArrayList<>(); + + places.add( + placeRepository.save( + Place.builder() + .name("์—ฌํ–‰์ง€1") + .address("์—ฌํ–‰์ง€1 ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€ ์„ค๋ช…") + .storedCount(100) + .city(cities.get(0)) + .build() + ) + ); + + places.add( + placeRepository.save( + Place.builder() + .name("์—ฌํ–‰์ง€2") + .address("์—ฌํ–‰์ง€2 ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€2 ์„ค๋ช…") + .storedCount(150) + .city(cities.get(0)) + .build() + ) + ); + + places.add( + placeRepository.save( + Place.builder() + .name("์—ฌํ–‰์ง€3") + .address("์—ฌํ–‰์ง€3 ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€3 ์„ค๋ช…") + .storedCount(115) + .city(cities.get(1)) + .build() + ) + ); + + places.add( + placeRepository.save( + Place.builder() + .name("์—ฌํ–‰์ง€4") + .address("์—ฌํ–‰์ง€4 ์ฃผ์†Œ") + .description("์—ฌํ–‰์ง€4 ์„ค๋ช…") + .storedCount(200) + .city(cities.get(1)) + .build() + ) + ); + + TripRecord tripRecord1 = tripRecordRepository.save( + TripRecord.builder() + .averageRating(4.0) + .title("์—ฌํ–‰ ํ›„๊ธฐ ์ œ๋ชฉ") + .content("์—ฌํ–‰ ํ›„๊ธฐ") + .countries("THAILAND") + .storeCount(100) + .expenseRangeType(ExpenseRangeType.ABOVE_300) + .member(memberRepository.save( + Member.builder() + .authority("ROLE_USER") + .email("member@email.com") + .password("password") + .nickname("member") + .build() + )) + .build() + ); + + TripRecord tripRecord2 = tripRecordRepository.save( + TripRecord.builder() + .averageRating(4.0) + .title("์—ฌํ–‰ ํ›„๊ธฐ ์ œ๋ชฉ") + .content("์—ฌํ–‰ ํ›„๊ธฐ") + .countries("JAPAN") + .storeCount(200) + .expenseRangeType(ExpenseRangeType.BELOW_100) + .member(memberRepository.save( + Member.builder() + .authority("ROLE_USER") + .email("member@email.com") + .password("password") + .nickname("member") + .build() + )) + .build() + ); + + TripRecord tripRecord3 = tripRecordRepository.save( + TripRecord.builder() + .averageRating(4.0) + .title("์—ฌํ–‰ ํ›„๊ธฐ ์ œ๋ชฉ") + .content("์—ฌํ–‰ ํ›„๊ธฐ") + .countries("KOREA") + .storeCount(150) + .expenseRangeType(ExpenseRangeType.BELOW_100) + .member(memberRepository.save( + Member.builder() + .authority("ROLE_USER") + .email("member@email.com") + .password("password") + .nickname("member") + .build() + )) + .build() + ); + + List schedules = new ArrayList<>(); + + schedules.add( + tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord1) + .place(places.get(0)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ) + ); + + schedules.add( + tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord1) + .place(places.get(1)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ) + ); + + schedules.add( + tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord2) + .place(places.get(2)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ) + ); + + schedules.add( + tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord2) + .place(places.get(3)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ) + ); + + schedules.add( + tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord3) + .place(places.get(0)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ) + ); + + schedules.add( + tripRecordScheduleRepository.save( + TripRecordSchedule.builder() + .tripRecord(tripRecord3) + .place(places.get(1)) + .dayNumber(1) + .ordering(1) + .tagType(ExternalLinkTagType.ACCOMMODATION_RESERVATION) + .tagUrl("tagUrl") + .build() + ) + ); + + for (int i = 0; i < schedules.size(); i++) { + tripRecordScheduleVideoRepository.save( + TripRecordScheduleVideo.builder() + .tripRecordSchedule(schedules.get(i)) + .thumbnailUrl("thumbnail" + i) + .videoUrl("videoUrl" + i) + .build() + ); + } + } + + @Test + void findNewestVideoList() { + //given + int size = 5; + + //when + List result = tripRecordScheduleVideoRepository.findNewestVideos(size); + + //then + assertThat(result).hasSize(size); + assertThat(result.get(0).memberName()).isEqualTo("member"); + assertThat(result.get(0).thumbnailUrl()).isEqualTo("thumbnail5"); + } + + @Test + void findNewestVideoListDomestic() { + //given + int size = 5; + + //when + List result = tripRecordScheduleVideoRepository.findNewestVideosDomestic(size); + + //then + assertThat(result).hasSize(2); + } + + @Test + void findNewestVideoListOverseas() { + //given + int size = 5; + + //when + List result = tripRecordScheduleVideoRepository.findNewestVideosOverseas(size); + + //then + assertThat(result).hasSize(4); + } +} \ No newline at end of file diff --git a/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_viewhistory/TripRecordViewHistoryRepositoryTest.java b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_viewhistory/TripRecordViewHistoryRepositoryTest.java new file mode 100644 index 00000000..bf536c5e --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/repository/triprecord_viewhistory/TripRecordViewHistoryRepositoryTest.java @@ -0,0 +1,49 @@ +package com.haejwo.tripcometrue.domain.triprecord.repository.triprecord_viewhistory; + +import com.haejwo.tripcometrue.config.TestQuerydslConfig; +import com.haejwo.tripcometrue.domain.triprecord.dto.query.TripRecordViewHistoryGroupByQueryDto; +import com.haejwo.tripcometrue.domain.triprecordViewHistory.repository.TripRecordViewHistoryRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@Slf4j +@Sql(scripts = "classpath:sql/test-data-insert.sql") +@ActiveProfiles("test") +@Import(TestQuerydslConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +class TripRecordViewHistoryRepositoryTest { + + @Autowired + private TripRecordViewHistoryRepository tripRecordViewHistoryRepository; + + + @Test + void findTopViewedMembers() { + // given + LocalDateTime start = LocalDateTime.of(2024, 1, 21, 0, 0, 0); + LocalDateTime end = LocalDateTime.of(2024, 1, 23, 23, 59, 59); + int size = 5; + + // when + List result = tripRecordViewHistoryRepository.findTopListMembers(start, end, size); + + // then + for (TripRecordViewHistoryGroupByQueryDto tripRecordViewHistoryGroupByQueryDto : result) { + log.info("{}", tripRecordViewHistoryGroupByQueryDto); + } + assertThat(result).hasSize(2); + assertThat(result.get(0).memberId()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/src/test/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordServiceTest.java b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordServiceTest.java new file mode 100644 index 00000000..d99d90fb --- /dev/null +++ b/src/test/java/com/haejwo/tripcometrue/domain/triprecord/service/TripRecordServiceTest.java @@ -0,0 +1,130 @@ +package com.haejwo.tripcometrue.domain.triprecord.service; + +import com.haejwo.tripcometrue.config.AbstractContainersSupport; +import com.haejwo.tripcometrue.config.DatabaseCleanUpAfterEach; +import com.haejwo.tripcometrue.domain.triprecord.dto.request.ModelAttribute.TripRecordSearchParamAttribute; +import com.haejwo.tripcometrue.domain.triprecord.dto.response.triprecord.TripRecordListItemResponseDto; +import com.haejwo.tripcometrue.domain.triprecord.entity.type.ExpenseRangeType; +import com.haejwo.tripcometrue.global.util.SliceResponseDto; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.jdbc.Sql; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@Slf4j +@Sql(scripts = "classpath:sql/test-data-insert.sql") +@DatabaseCleanUpAfterEach +@SpringBootTest +class TripRecordServiceTest extends AbstractContainersSupport { + + @Autowired + private TripRecordService tripRecordService; + + @Test + void findTripRecordList_bySearchParamAttribute_cityName() { + // given + PageRequest pageRequest = PageRequest.of(0, 2, + Sort.by(Sort.Order.desc("averageRating"), Sort.Order.desc("storeCount"))); + String cityName = "๋ฐฉ์ฝ•"; + TripRecordSearchParamAttribute paramAttribute = new TripRecordSearchParamAttribute(cityName, null, null, null); + + // when + SliceResponseDto result = tripRecordService.findTripRecordList(paramAttribute, pageRequest); + + // then + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0).tripRecordId()).isEqualTo(1); + assertThat(result.totalCount()).isEqualTo(1); + assertThat(result.currentPageNum()).isEqualTo(0); + assertThat(result.first()).isTrue(); + assertThat(result.last()).isTrue(); + } + + @Test + void findTripRecordList_bySearchParamAttribute_cityIdTotalDays() { + // given + PageRequest pageRequest = PageRequest.of(0, 2, + Sort.by(Sort.Order.desc("storeCount"), Sort.Order.desc("averageRating"))); + Long cityId = 3L; + String totalDays = "etc"; + TripRecordSearchParamAttribute paramAttribute = new TripRecordSearchParamAttribute(null, null, cityId, totalDays); + + // when + SliceResponseDto result = tripRecordService.findTripRecordList(paramAttribute, pageRequest); + + // then + assertThat(result.content()).hasSize(1); + assertThat(result.content().get(0).tripRecordId()).isEqualTo(3); + assertThat(result.totalCount()).isEqualTo(1); + assertThat(result.currentPageNum()).isEqualTo(0); + assertThat(result.first()).isTrue(); + assertThat(result.last()).isTrue(); + } + + @Test + void findTopTripRecordList_requestTypeDomestic() { + // given + String type = "domestic"; + + // when + List result = tripRecordService.findTopTripRecordList(type); + + // then + assertThat(result).hasSize(1); + } + + @Test + void findTopTripRecordList_requestTypeOverseas() { + // given + String type = "overseas"; + + // when + List result = tripRecordService.findTopTripRecordList(type); + + // then + assertThat(result).hasSize(3); + } + + @Test + void listTripRecordsByHashtag() { + // given + String hashtag = "์ž์œ ์—ฌํ–‰"; + PageRequest pageRequest = PageRequest.of(0, 2, + Sort.by(Sort.Order.desc("averageRating"), Sort.Order.desc("storeCount"))); + + // when + SliceResponseDto result = tripRecordService.listTripRecordsByHashtag(hashtag, pageRequest); + + // then + assertThat(result.content()).hasSize(2); + assertThat(result.currentPageNum()).isEqualTo(0); + assertThat(result.pageSize()).isEqualTo(2); + assertThat(result.first()).isTrue(); + assertThat(result.last()).isFalse(); + } + + @Test + void listTripRecordsByExpenseRangeType() { + // given + ExpenseRangeType expenseRangeType = ExpenseRangeType.BELOW_200; + PageRequest pageRequest = PageRequest.of(0, 2, + Sort.by(Sort.Order.desc("averageRating"), Sort.Order.desc("storeCount"))); + + // when + SliceResponseDto result = tripRecordService.listTripRecordsByExpenseRangeType(expenseRangeType, pageRequest); + + // then + assertThat(result.content()).hasSize(1); + assertThat(result.currentPageNum()).isEqualTo(0); + assertThat(result.pageSize()).isEqualTo(2); + assertThat(result.first()).isTrue(); + assertThat(result.last()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 00000000..7f072b4f --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,86 @@ +spring: + config: + import: optional:file:.env[.properties] + + datasource: + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + url: jdbc:tc:mysql:8:///tripcometrue?characterEncoding=UTF-8&serverTimezone=Asia/Seoul + + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQLDialect + default_batch_fetch_size: 200 + + data: + redis: + host: localhost + port: 6379 + + security: + oauth2: + client: + registration: + google: + client-id: ${GOOGLE_ID} + client-secret: ${GOOGLE_KEY} + scope: + - email + - profile + + naver: + client-id: ${NAVER_ID} + client-secret: ${NAVER_KEY} + scope: + - name + - email + client-name: Naver + authorization-grant-type: authorization_code + redirect-uri: http://localhost:8080/login/oauth2/code/naver + + kakao: + client-id: ${KAKAO_ID} + client-secret: ${KAKAO_KEY} + client-authentication-method: client_secret_post + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + authorization-grant-type: authorization_code + client-name: Kakao + scope: + - profile_nickname + - account_email + - profile_image + + provider: + naver: + authorization-uri: https://nid.naver.com/oauth2.0/authorize + token-uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user-name-attribute: response + 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 + +logging: + level: + "[org.springframework.security]": DEBUG + +cloud: + aws: + credentials: + accessKey: ${AWS_ACCESS_KEY} + secretKey: ${AWS_SECRET_KEY} + region: + static: ${AWS_S3_REGION} + s3: + bucket: ${AWS_S3_DEV_BUCKET} + +exchange-rate: + api: + url: ${EXCHANGE_RATE_API_URL} + key: ${EXCHANGE_RATE_API_KEY} diff --git a/src/test/resources/sql/test-data-insert.sql b/src/test/resources/sql/test-data-insert.sql new file mode 100644 index 00000000..c5dd9023 --- /dev/null +++ b/src/test/resources/sql/test-data-insert.sql @@ -0,0 +1,148 @@ +/* member */ +insert into member (member_id, created_at, updated_at, authority, email, nickname, password, member_rating, profile_image, introduction) +values (1, now(), now(), 'ROLE_USER', 'member1@email.com', '์น˜์•™๋งˆ์ด', 'password1', 4.0, 'profile image url', '์น˜์•™๋งˆ์ด ์†Œ๊ฐœ๊ธ€'); + +insert into member (member_id, created_at, updated_at, authority, email, nickname, password, member_rating, profile_image, introduction) +values (2, now(), now(), 'ROLE_USER', 'member2@email.com', '์น˜์•™ ๋งˆ์ด ๋Ÿฌ๋ฒ„', 'password2', 2.8, 'profile image url', '์น˜์•™ ๋งˆ์ด ๋Ÿฌ๋ฒ„ ์†Œ๊ฐœ๊ธ€'); + +insert into member (member_id, created_at, updated_at, authority, email, nickname, password, member_rating, profile_image, introduction) +values (3, now(), now(), 'ROLE_USER', 'member3@email.com', 'ํŒŒ๋ฆฌ์ง€์•ต', 'password3', 4.3, 'profile image url', 'ํŒŒ๋ฆฌ์ง€์•ต ์†Œ๊ฐœ๊ธ€'); + +/* city */ +insert into city (city_id, created_at, updated_at, country, currency, language, name, time_difference, visa, voltage, + weather_description, weather_recommendation, image_url, store_count) +values (1, now(), now(), 'THAILAND', 'THB', 'ํƒœ๊ตญ์–ด', '๋ฐฉ์ฝ•', '2์‹œ๊ฐ„ ๋Š๋ฆผ', '90์ผ ๋ฌด๋น„์ž ์ฒด๋ฅ˜', '220V', '๋ฐฉ์ฝ• ๋‚ ์”จ ์„ค๋ช…', '๋ฐฉ์ฝ• ๋‚ ์”จ ์ถ”์ฒœ', '๋ฐฉ์ฝ• ์ด๋ฏธ์ง€', 45); + +insert into city (city_id, created_at, updated_at, country, currency, language, name, time_difference, visa, voltage, + weather_description, weather_recommendation, image_url, store_count) +values (2, now(), now(), 'JAPAN', 'JPY', '์ผ๋ณธ์–ด', '๋„์ฟ„', '์‹œ์ฐจ ์—†์Œ', '90์ผ ๋ฌด๋น„์ž ์ฒด๋ฅ˜', '220V', '๋„์ฟ„ ๋‚ ์”จ ์„ค๋ช…', '๋„์ฟ„ ๋‚ ์”จ ์ถ”์ฒœ', '๋„์ฟ„ ์ด๋ฏธ์ง€', 100); + +insert into city (city_id, created_at, updated_at, country, currency, language, name, time_difference, visa, voltage, + weather_description, weather_recommendation, image_url, store_count) +values (3, now(), now(), 'USA', 'USD', '์˜์–ด', '๋‰ด์š•', '14์‹œ๊ฐ„ ๋Š๋ฆผ', '90์ผ ๋ฌด๋น„์ž ์ฒด๋ฅ˜', '110V', '๋‰ด์š• ๋‚ ์”จ ์„ค๋ช…', '๋‰ด์š• ๋‚ ์”จ ์ถ”์ฒœ', '๋‰ด์š• ์ด๋ฏธ์ง€', 95); + +insert into city (city_id, created_at, updated_at, country, language, name, weather_description, weather_recommendation, image_url, store_count) +values (4, now(), now(), 'KOREA', 'ํ•œ๊ตญ์–ด', '๋ถ€์‚ฐ', '๋ถ€์‚ฐ ๋‚ ์”จ ์„ค๋ช…', '๋ถ€์‚ฐ ๋‚ ์”จ ์ถ”์ฒœ', '๋ถ€์‚ฐ ์ด๋ฏธ์ง€', 88); + +/* place */ +insert into place (place_id, created_at, updated_at, address, city_id, description, name, stored_count, latitude, longitude, comment_count, review_count) +values (1, now(), now(), '์—ฌํ–‰์ง€1 ์ฃผ์†Œ', 1, '์—ฌํ–‰์ง€1 ์„ค๋ช…', '์—ฌํ–‰์ง€1', 30, 130.7, 45.6, 10, 10); + +insert into place (place_id, created_at, updated_at, address, city_id, description, name, stored_count, latitude, longitude, comment_count, review_count) +values (2, now(), now(), '์—ฌํ–‰์ง€2 ์ฃผ์†Œ', 2, '์—ฌํ–‰์ง€2 ์„ค๋ช…', '์—ฌํ–‰์ง€2', 45, 130.7, 45.6, 20, 10); + +insert into place (place_id, created_at, updated_at, address, city_id, description, name, stored_count, latitude, longitude, comment_count, review_count) +values (3, now(), now(), '์—ฌํ–‰์ง€3 ์ฃผ์†Œ', 3, '์—ฌํ–‰์ง€3 ์„ค๋ช…', '์—ฌํ–‰์ง€3', 100, 130.7, 45.6, 15, 10); + +insert into place (place_id, created_at, updated_at, address, city_id, description, name, stored_count, latitude, longitude, comment_count, review_count) +values (4, now(), now(), '์—ฌํ–‰์ง€4 ์ฃผ์†Œ', 4, '์—ฌํ–‰์ง€4 ์„ค๋ช…', '์—ฌํ–‰์ง€4', 20, 130.7, 45.6, 33, 10); + +insert into place (place_id, created_at, updated_at, address, city_id, description, name, stored_count, latitude, longitude, comment_count, review_count) +values (5, now(), now(), '์—ฌํ–‰์ง€5 ์ฃผ์†Œ', 1, '์—ฌํ–‰์ง€5 ์„ค๋ช…', '์—ฌํ–‰์ง€5', 50, 130.7, 45.6, 14, 15); + + +/* trip_record */ +insert into trip_record (trip_record_id, average_rating, content, countries, expense_range_type, title, total_days, trip_end_day, trip_start_day, view_count, + created_at, updated_at, member_id, comment_count, review_count, store_count) +values (1, 3.5, '์—ฌํ–‰ ์ปจํ…์ธ 1', 'THAILAND', 'BELOW_100', '๋ฐฉ์ฝ• ์—ฌํ–‰ ์ œ๋ชฉ', 4, '2024-01-25', '2024-01-22', 100, now(), now(), 1, 5, 3, 30); + +insert into trip_record (trip_record_id, average_rating, content, countries, expense_range_type, title, total_days, trip_end_day, trip_start_day, view_count, + created_at, updated_at, member_id, comment_count, review_count, store_count) +values (2, 4.1, '์—ฌํ–‰ ์ปจํ…์ธ 2', 'JAPAN', 'BELOW_200', '๋„์ฟ„ ์—ฌํ–‰ ์ œ๋ชฉ', 4, '2024-01-25', '2024-01-22', 100, now(), now(), 1, 5, 3, 50); + +insert into trip_record (trip_record_id, average_rating, content, countries, expense_range_type, title, total_days, trip_end_day, trip_start_day, view_count, + created_at, updated_at, member_id, comment_count, review_count, store_count) +values (3, 2.9, '์—ฌํ–‰ ์ปจํ…์ธ 4', 'USA', 'BELOW_300', '๋‰ด์š• ์—ฌํ–‰ ์ œ๋ชฉ', 10, '2024-02-01', '2024-02-10', 400, now(), now(), 3, 5, 3, 70); + +insert into trip_record (trip_record_id, average_rating, content, countries, expense_range_type, title, total_days, trip_end_day, trip_start_day, view_count, + created_at, updated_at, member_id, comment_count, review_count, store_count) +values (4, 2.9, '์—ฌํ–‰ ์ปจํ…์ธ 3', 'KOREA', 'BELOW_50', '๋ถ€์‚ฐ ์—ฌํ–‰ ์ œ๋ชฉ', 2, '2024-01-30', '2024-01-29', 200, now(), now(), 2, 5, 3, 45); + + +/* trip_record_schedule */ +insert into trip_record_schedule (trip_record_schedule_id, content, day_number, ordering, trip_record_id, created_at, updated_at, place_id) +values (1, '์—ฌํ–‰ ์Šค์ผ€์ค„1', 1, 1, 1, now(), now(), 1); + +insert into trip_record_schedule (trip_record_schedule_id, content, day_number, ordering, trip_record_id, created_at, updated_at, place_id) +values (2, '์—ฌํ–‰ ์Šค์ผ€์ค„2', 1, 2, 1, now(), now(), 5); + +insert into trip_record_schedule (trip_record_schedule_id, content, day_number, ordering, trip_record_id, created_at, updated_at, place_id) +values (3, '์—ฌํ–‰ ์Šค์ผ€์ค„3', 1, 1, 2, now(), now(), 2); + +insert into trip_record_schedule (trip_record_schedule_id, content, day_number, ordering, trip_record_id, created_at, updated_at, place_id) +values (4, '์—ฌํ–‰ ์Šค์ผ€์ค„4', 1, 1, 3, now(), now(), 3); + +insert into trip_record_schedule (trip_record_schedule_id, content, day_number, ordering, trip_record_id, created_at, updated_at, place_id) +values (5, '์—ฌํ–‰ ์Šค์ผ€์ค„5', 1, 1, 4, now(), now(), 4); + +/* trip_record_schedule_video */ +insert into trip_record_schedule_video (trip_record_schedule_video_id, created_at, updated_at, video_url, trip_schedule_id, thumbnail_url) +values (1, now(), now(), '์‡ผ์ธ 1', 1, '์‡ผ์ธ 1 ์ธ๋„ค์ผ'); + +insert into trip_record_schedule_video (trip_record_schedule_video_id, created_at, updated_at, video_url, trip_schedule_id, thumbnail_url) +values (2, now(), now(), '์‡ผ์ธ 2', 2, '์‡ผ์ธ 2 ์ธ๋„ค์ผ'); + +insert into trip_record_schedule_video (trip_record_schedule_video_id, created_at, updated_at, video_url, trip_schedule_id, thumbnail_url) +values (3, now(), now(), '์‡ผ์ธ 3', 3, '์‡ผ์ธ 3 ์ธ๋„ค์ผ'); + +insert into trip_record_schedule_video (trip_record_schedule_video_id, created_at, updated_at, video_url, trip_schedule_id, thumbnail_url) +values (4, now(), now(), '์‡ผ์ธ 4', 4, '์‡ผ์ธ 4 ์ธ๋„ค์ผ'); + +insert into trip_record_schedule_video (trip_record_schedule_video_id, created_at, updated_at, video_url, trip_schedule_id, thumbnail_url) +values (5, now(), now(), '์‡ผ์ธ 5', 5, '์‡ผ์ธ 5 ์ธ๋„ค์ผ'); + +insert into trip_record_schedule_video (trip_record_schedule_video_id, created_at, updated_at, video_url, trip_schedule_id, thumbnail_url) +values (6, now(), now(), '์‡ผ์ธ 6', 1, '์‡ผ์ธ 6 ์ธ๋„ค์ผ'); + +/* trip_record_tag */ +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (1, now(), now(), '์—ฐ์ธ๋ผ๋ฆฌ', 1); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (2, now(), now(), '์ž์œ ์—ฌํ–‰', 1); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (3, now(), now(), '์ธ๊ธฐ์—ฌํ–‰์ง€', 1); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (4, now(), now(), '์นœ๊ตฌ๋ผ๋ฆฌ', 2); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (5, now(), now(), '๊ณ ์˜ˆ์‚ฐ', 2); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (6, now(), now(), '์ž์œ ์—ฌํ–‰', 3); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (7, now(), now(), '๊ณ ์˜ˆ์‚ฐ', 3); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (8, now(), now(), '์ž์œ ์—ฌํ–‰', 3); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (9, now(), now(), '๊ณ ์˜ˆ์‚ฐ', 3); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (10, now(), now(), '์ž์œ ์—ฌํ–‰', 3); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (11, now(), now(), 'ํ˜ผ์ž์—ฌํ–‰', 4); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (12, now(), now(), '์ž์œ ์—ฌํ–‰', 4); + +insert into trip_record_tag (trip_record_tag_id, created_at, updated_at, hash_tag_type, trip_record_id) +values (13, now(), now(), '๋ง›์ง‘', 4); + +/* trip_record_view_history */ +insert into trip_record_view_history (created_at, updated_at, member_id, trip_record_id) +values ('2024-01-21T10:00:00', '2024-01-21T10:00:00', 2, 1); + +insert into trip_record_view_history (created_at, updated_at, member_id, trip_record_id) +values ('2024-01-21T22:00:00', '2024-01-22T22:00:00', 3, 1); + +insert into trip_record_view_history (created_at, updated_at, member_id, trip_record_id) +values ('2024-01-21T23:59:00', '2024-01-21T23:59:00', 2, 2); + +insert into trip_record_view_history (created_at, updated_at, member_id, trip_record_id) +values ('2024-01-21T15:00:00', '2024-01-23T23:59:59', 2, 3);