diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b111724e..1266bc27 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,15 +1,19 @@ -# ๐Ÿ“‹ ์—ฐ๊ด€๋œ ์ด์Šˆ ๋ฒˆํ˜ธ +## ๐Ÿ“‹ ์—ฐ๊ด€๋œ ์ด์Šˆ ๋ฒˆํ˜ธ - close #issueNumber -# ๐Ÿ›  ๊ตฌํ˜„ ์‚ฌํ•ญ +## ๐Ÿ›  ๊ตฌํ˜„ ์‚ฌํ•ญ - ๋‚ด์šฉ์„ ์ ์–ด์ฃผ์„ธ์š”. -# ๐Ÿœ ์Šคํฌ๋ฆฐ์ƒท +## ๐Ÿ“š ๋ณ€๊ฒฝ ์‚ฌํ•ญ - - ๋™์˜์ƒ, ์‚ฌ์ง„, ๋กœ๊ทธ ๋“ฑ๋“ฑ - - ex) ํ์•Œ ์„ฑ๊ณต ์ด๋ฏธ์ง€, ์Šค์›จ๊ฑฐ, ํฌ์ŠคํŠธ๋งจ ๋“ฑ +- ๋‚ด์šฉ์„ ์ ์–ด์ฃผ์„ธ์š”. + +## ๐Ÿœ ์Šคํฌ๋ฆฐ์ƒท + +- ๋™์˜์ƒ, ์‚ฌ์ง„, ๋กœ๊ทธ ๋“ฑ๋“ฑ +- ex) ํ์•Œ ์„ฑ๊ณต ์ด๋ฏธ์ง€, ์Šค์›จ๊ฑฐ, ํฌ์ŠคํŠธ๋งจ ๋“ฑ ## ๐Ÿ’ฌ To Reviewers diff --git a/.github/workflows/depoly-desktop.yml b/.github/workflows/depoly-desktop.yml new file mode 100644 index 00000000..c64979c3 --- /dev/null +++ b/.github/workflows/depoly-desktop.yml @@ -0,0 +1,86 @@ +name: Build and Deploy to Local Ubuntu using Docker + +on: + push: + branches: + - dev + +env: + DOCKER_IMAGE_NAME: with-bee-dev + LOCAL_UBUNTU_HOST: ${{ secrets.LOCAL_UBUNTU_HOST }} + LOCAL_UBUNTU_SSH_USER: ubuntu + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_HUB: ${{ secrets.DOCKER_USERNAME }}/withbee-dev + SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.cj.jdbc.Driver + +jobs: + build-and-push-docker: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + + - name: Grant execute permissions to gradlew + run: chmod +x ./gradlew + + - name: Create necessary directories + run: | + mkdir -p ./src/main/resources + - name: Create application.properties + run: | + echo "${{ secrets.APPLICATION_DEV }}" > ./src/main/resources/application.properties + - name: Create aws.properties + run: | + echo "${{ secrets.AWS_PROPERTIES }}" > ./src/main/resources/aws.properties + + - name: Build with Gradle Wrapper (without tests) + run: ./gradlew build -x test + + - name: Build Docker image + run: | + docker build . --file Dockerfile --tag ${{ secrets.DOCKER_USERNAME }}/withbee-dev:latest + + - name: Login to Docker Hub using Access Token + run: echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + + - name: Push Docker image + run: docker push ${{ secrets.DOCKER_USERNAME }}/withbee-dev:latest + + deploy-to-ubuntu: + runs-on: ubuntu-latest + needs: build-and-push-docker + steps: + - name: Deploy to Local Ubuntu + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.LOCAL_UBUNTU_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} + script: | + # Docker ๋กœ๊ทธ์ธ + echo "${{ secrets.DOCKER_HUB_TOKEN }}" | sudo docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + # 8080 ํฌํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์ปจํ…Œ์ด๋„ˆ ์ฐพ๊ธฐ + CONTAINER_ID=$(sudo docker ps -q --filter "publish=8080-8080") + if [ ! -z "$CONTAINER_ID" ]; then + sudo docker stop $CONTAINER_ID + sudo docker rm $CONTAINER_ID + fi + + # ๋ชจ๋“  ์ข…๋ฃŒ๋œ(exited) ์ปจํ…Œ์ด๋„ˆ ์‚ญ์ œ + sudo docker container prune -f + + # ์ตœ์‹  ์ด๋ฏธ์ง€ ํ’€๋ง + sudo docker pull "${{ secrets.DOCKER_USERNAME }}/withbee-dev:latest" + + # ์ƒˆ๋กœ์šด ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ + sudo docker run --name with-bee-dev -d -p 8080:8080 -e TZ=Asia/Seoul "${{ secrets.DOCKER_USERNAME }}/withbee-dev:latest" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 44fc9570..1c07ce58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Created by https://www.toptal.com/developers/gitignore/api/windows,macos,git,java,gradle,intellij,eclipse,netbeans # Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,git,java,gradle,intellij,eclipse,netbeans +# ๋”๋ฏธ๋ฐ์ดํ„ฐ ํŒŒ์ผ +src/main/java/withbeetravel/DataLoader.java + ### Eclipse ### .metadata bin/ @@ -135,7 +138,7 @@ cmake-build-*/ *.iws # IntelliJ -application.properties +*.properties out/ .idea diff --git a/build.gradle b/build.gradle index c94d529f..7d7c4a0b 100644 --- a/build.gradle +++ b/build.gradle @@ -28,14 +28,43 @@ 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' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + // Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' + // S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // log4jdbc + implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' + + //JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //validation + implementation group: 'jakarta.validation', name: 'jakarta.validation-api', version: '3.1.0' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // OpenAI API ํ˜ธ์ถœ์„ ์œ„ํ•œ HTTP ํด๋ผ์ด์–ธํŠธ + implementation 'com.squareup.okhttp3:okhttp:4.9.1' + // JSON ์ฒ˜๋ฆฌ + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'org.json:json:20230227' + + // REST - Assured + testImplementation 'io.rest-assured:rest-assured:5.5.0' + } tasks.named('test') { useJUnitPlatform() } + + + diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/src/main/java/withbeetravel/DataLoader.java b/src/main/java/withbeetravel/DataLoader.java new file mode 100644 index 00000000..e41462ec --- /dev/null +++ b/src/main/java/withbeetravel/DataLoader.java @@ -0,0 +1,199 @@ +package withbeetravel; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.bcrypt.BCrypt; +import org.springframework.stereotype.Component; +import withbeetravel.domain.*; +import withbeetravel.repository.*; + +import static withbeetravel.domain.RoleType.ADMIN; +import static withbeetravel.domain.RoleType.USER; + +//@Component +@RequiredArgsConstructor +public class DataLoader implements CommandLineRunner { + + private final UserRepository userRepository; + private final AccountRepository accountRepository; + + @Override + public void run(String... args) throws Exception { + // User ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + User user1 = User.builder() + .email("admin@admin") + .password(BCrypt.hashpw("password123!", BCrypt.gensalt())) + .pinNumber("123456") + .name("๊ด€๋ฆฌ์ž") + .roleType(ADMIN) + .pinLocked(false) + .failedPinCount(0) + .profileImage(1) + .build(); + + User user2 = User.builder() + .email("1@naver.com") + .password(BCrypt.hashpw("password123!", BCrypt.gensalt())) + .pinNumber("123456") + .name("๊ณต์†Œ์—ฐ") + .roleType(USER) + .pinLocked(false) + .failedPinCount(0) + .profileImage(1) + .build(); + + User user3 = User.builder() + .email("2@naver.com") + .password(BCrypt.hashpw("password123!", BCrypt.gensalt())) + .pinNumber("123456") + .name("๊ณต์˜ˆ์ง„") + .roleType(USER) + .pinLocked(false) + .failedPinCount(0) + .profileImage(2) + .build(); + + User user4 = User.builder() + .email("3@naver.com") + .password(BCrypt.hashpw("password123!", BCrypt.gensalt())) + .pinNumber("123456") + .name("๊น€ํ˜ธ์ฒ ") + .roleType(USER) + .pinLocked(false) + .failedPinCount(0) + .profileImage(3) + .build(); + + User user5 = User.builder() + .email("4@naver.com") + .password(BCrypt.hashpw("password123!", BCrypt.gensalt())) + .pinNumber("123456") + .name("์ด๋„์ด") + .roleType(USER) + .pinLocked(false) + .failedPinCount(0) + .profileImage(4) + .build(); + + User user6 = User.builder() + .email("5@naver.com") + .password(BCrypt.hashpw("password123!", BCrypt.gensalt())) + .pinNumber("123456") + .name("์œ ์Šน์•„") + .roleType(USER) + .pinLocked(false) + .failedPinCount(0) + .profileImage(5) + .build(); + + userRepository.save(user1); + userRepository.save(user2); + userRepository.save(user3); + userRepository.save(user4); + userRepository.save(user5); + userRepository.save(user6); + + // Account ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + Account account1 = Account.builder() + .user(user1) + .accountNumber("1483920493821") + .balance(100000000) + .product(Product.WONํ†ต์žฅ) + .isConnectedWibeeCard(true) + .build(); +// +// Account account2 = Account.builder() +// .user(user2) +// .accountNumber("1111111111111") +// .balance(300000) +// .product(Product.WONํ†ต์žฅ) +// .isConnectedWibeeCard(true) +// .build(); +// +// Account account3 = Account.builder() +// .user(user3) +// .accountNumber("2222222222222") +// .balance(400000) +// .product(Product.WONํ†ต์žฅ) +// .isConnectedWibeeCard(true) +// .build(); +// +// Account account4 = Account.builder() +// .user(user4) +// .accountNumber("3333333333333") +// .balance(500000) +// .product(Product.์šฐ๋ฆฌ๋‹ท์ปดํ†ต์žฅ) +// .isConnectedWibeeCard(true) +// .build(); +// +// Account account5 = Account.builder() +// .user(user3) +// .accountNumber("4444444444444") +// .balance(600000) +// .product(Product.์šฐ๋ฆฌ๋‹ท์ปดํ†ต์žฅ) +// .isConnectedWibeeCard(false) +// .build(); +// +// Account account6 = Account.builder() +// .user(user4) +// .accountNumber("5555555555555") +// .balance(700000) +// .product(Product.์šฐ๋ฆฌ์•„์ดํ–‰๋ณตํ†ต์žฅ) +// .isConnectedWibeeCard(false) +// .build(); +// +// Account account7 = Account.builder() +// .user(user5) +// .accountNumber("6666666666666") +// .balance(800000) +// .product(Product.์œผ์“ฑํ†ต์žฅ) +// .isConnectedWibeeCard(true) +// .build(); +// +// Account account8 = Account.builder() +// .user(user5) +// .accountNumber("7777777777777") +// .balance(900000) +// .product(Product.WONํ†ต์žฅ) +// .isConnectedWibeeCard(false) +// .build(); +// +// Account account9 = Account.builder() +// .user(user6) +// .accountNumber("8888888888888") +// .balance(850000) +// .product(Product.WONํ†ต์žฅ) +// .isConnectedWibeeCard(false) +// .build(); + + accountRepository.save(account1); +// accountRepository.save(account2); +// accountRepository.save(account3); +// accountRepository.save(account4); +// accountRepository.save(account5); +// accountRepository.save(account6); +// accountRepository.save(account7); +// accountRepository.save(account8); +// accountRepository.save(account9); + + // ์—ฐ๊ฒฐ๋œ ๊ณ„์ขŒ ์—…๋ฐ์ดํŠธ (์—ฐ๊ฒฐ ๊ณ„์ขŒ ์„ค์ •) + user1.updateConnectedAccount(account1); + user1.updateWibeeCardAccount(account1); +// user2.updateConnectedAccount(account2); +// user2.updateWibeeCardAccount(account2); +// user3.updateConnectedAccount(account3); +// user3.updateWibeeCardAccount(account3); +// user4.updateConnectedAccount(account4); +// user4.updateWibeeCardAccount(account4); +// user5.updateConnectedAccount(account7); +// user5.updateWibeeCardAccount(account7); +// user6.updateConnectedAccount(account9); + + userRepository.save(user1); +// userRepository.save(user2); +// userRepository.save(user3); +// userRepository.save(user4); +// userRepository.save(user5); +// userRepository.save(user6); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/WithbeetravelApplication.java b/src/main/java/withbeetravel/WithbeetravelApplication.java index 8a9b08d8..88933022 100644 --- a/src/main/java/withbeetravel/WithbeetravelApplication.java +++ b/src/main/java/withbeetravel/WithbeetravelApplication.java @@ -2,12 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class WithbeetravelApplication { - public static void main(String[] args) { - SpringApplication.run(WithbeetravelApplication.class, args); - } + public static void main(String[] args) {SpringApplication.run(WithbeetravelApplication.class, args);} } diff --git a/src/main/java/withbeetravel/aspect/BankingAccessAspect.java b/src/main/java/withbeetravel/aspect/BankingAccessAspect.java new file mode 100644 index 00000000..6d13f1ae --- /dev/null +++ b/src/main/java/withbeetravel/aspect/BankingAccessAspect.java @@ -0,0 +1,57 @@ +package withbeetravel.aspect; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import withbeetravel.domain.Account; +import withbeetravel.domain.User; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.BankingErrorCode; +import withbeetravel.repository.AccountRepository; +import withbeetravel.security.UserAuthorizationUtil; + +@Aspect +@Component +@RequiredArgsConstructor +public class BankingAccessAspect { + private final AccountRepository accountRepository; + + // ํ˜„์žฌ ๋กœ๊ทธ์ธ ๋œ userId + + @Before("@annotation(checkBankingAccess)") // @CheckBankingAccess ์• ๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „์— ์‹คํ–‰ + public void checkAccess(JoinPoint joinPoint, CheckBankingAccess checkBankingAccess) { + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + // @CheckBankingAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์†Œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ accountId ์ถ”์ถœ + Long accountId = getAccountIdFromArgs(joinPoint, checkBankingAccess.accountIdParam()); + + // accountId์— ํ•ด๋‹นํ•˜๋Š” ๊ณ„์ขŒ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + // ๊ถŒํ•œ ๊ฒ€์‚ฌ: ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž์™€ ๊ณ„์ขŒ ์†Œ์œ ์ž๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ + Long accountUserId = account.getUser().getId(); // Account ๊ฐ์ฒด์— User ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ • + if (!userId.equals(accountUserId)) { + throw new CustomException(BankingErrorCode.HISTORY_ACCESS_FORBIDDEN); + } + } + + private Long getAccountIdFromArgs(JoinPoint joinPoint, String accountIdParam) { + // ๋ฉ”์†Œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์ด๋ฆ„์„ ๋งคํ•‘ํ•˜์—ฌ accountId๋ฅผ ์ฐพ์•„ ๋ฐ˜ํ™˜ + String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); + Object[] args = joinPoint.getArgs(); + + for (int i = 0; i < paramNames.length; i++) { + if (paramNames[i].equals(accountIdParam) && args[i] instanceof Long) { + return (Long) args[i]; + } + } + + // @CheckBankingAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์„œ๋“œ์—์„œ "accountId"๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ + throw new IllegalArgumentException("accountId ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/src/main/java/withbeetravel/aspect/CheckBankingAccess.java b/src/main/java/withbeetravel/aspect/CheckBankingAccess.java new file mode 100644 index 00000000..642ee110 --- /dev/null +++ b/src/main/java/withbeetravel/aspect/CheckBankingAccess.java @@ -0,0 +1,13 @@ +package withbeetravel.aspect; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) // ๋ฉ”์†Œ๋“œ๋‹จ์—์„œ ์‚ฌ์šฉ +@Retention(RetentionPolicy.RUNTIME) // ๋Ÿฐํƒ€์ž„ ์‹œ์ ์— ์œ ์ง€ +public @interface CheckBankingAccess { + + String accountIdParam() default "accountId"; +} diff --git a/src/main/java/withbeetravel/aspect/CheckTravelAccess.java b/src/main/java/withbeetravel/aspect/CheckTravelAccess.java new file mode 100644 index 00000000..fd5ac6af --- /dev/null +++ b/src/main/java/withbeetravel/aspect/CheckTravelAccess.java @@ -0,0 +1,18 @@ +package withbeetravel.aspect; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Travel์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ์ ์šฉํ•  ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉ + */ + +@Target(ElementType.METHOD) // ๋ฉ”์†Œ๋“œ๋‹จ์—์„œ ์‚ฌ์šฉ +@Retention(RetentionPolicy.RUNTIME) // ๋Ÿฐํƒ€์ž„ ์‹œ์ ์— ์œ ์ง€ +public @interface CheckTravelAccess { + + // ํ•ด๋‹น ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ ๋ฉ”์†Œ๋“œ์—์„œ ๊ฐ€์ ธ์˜ฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„ + String travelIdParam() default "travelId"; +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/aspect/CheckTravelAndSharedPaymentAccess.java b/src/main/java/withbeetravel/aspect/CheckTravelAndSharedPaymentAccess.java new file mode 100644 index 00000000..1e2d5bc5 --- /dev/null +++ b/src/main/java/withbeetravel/aspect/CheckTravelAndSharedPaymentAccess.java @@ -0,0 +1,19 @@ +package withbeetravel.aspect; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Travel๊ณผ shared payment์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ์ ์šฉํ•  ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉ + */ + +@Target(ElementType.METHOD) // ๋ฉ”์†Œ๋“œ๋‹จ์—์„œ ์‚ฌ์šฉ +@Retention(RetentionPolicy.RUNTIME) // ๋Ÿฐํƒ€์ž„ ์‹œ์ ์— ์œ ์ง€ +public @interface CheckTravelAndSharedPaymentAccess { + + // ํ•ด๋‹น ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ ๋ฉ”์†Œ๋“œ์—์„œ ๊ฐ€์ ธ์˜ฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„ + String travelIdParam() default "travelId"; + String sharedPaymentIdParam() default "sharedPaymentId"; +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/aspect/PaymentValidation.java b/src/main/java/withbeetravel/aspect/PaymentValidation.java new file mode 100644 index 00000000..39658524 --- /dev/null +++ b/src/main/java/withbeetravel/aspect/PaymentValidation.java @@ -0,0 +1,10 @@ +package withbeetravel.aspect; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface PaymentValidation {} \ No newline at end of file diff --git a/src/main/java/withbeetravel/aspect/PaymentValidationAspect.java b/src/main/java/withbeetravel/aspect/PaymentValidationAspect.java new file mode 100644 index 00000000..ca27c3d9 --- /dev/null +++ b/src/main/java/withbeetravel/aspect/PaymentValidationAspect.java @@ -0,0 +1,34 @@ +package withbeetravel.aspect; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import withbeetravel.dto.request.payment.SharedPaymentSearchRequest; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.ValidationErrorCode; + +import java.time.LocalDate; + +@Aspect +@Component +@RequiredArgsConstructor +public class PaymentValidationAspect { + + @Before("@annotation(paymentValidation)") + public void validatePaymentRequest(JoinPoint joinPoint, PaymentValidation paymentValidation) { + Object[] args = joinPoint.getArgs(); + for (Object arg : args) { + if (arg instanceof SharedPaymentSearchRequest request) { + validateDateRange(request.getStartDate(), request.getEndDate()); + } + } + } + + private void validateDateRange(LocalDate startDate, LocalDate endDate) { + if (startDate != null && endDate != null && startDate.isAfter(endDate)) { + throw new CustomException(ValidationErrorCode.DATE_RANGE_ERROR); + } + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/aspect/TravelAccessAspect.java b/src/main/java/withbeetravel/aspect/TravelAccessAspect.java new file mode 100644 index 00000000..10a34f02 --- /dev/null +++ b/src/main/java/withbeetravel/aspect/TravelAccessAspect.java @@ -0,0 +1,59 @@ +package withbeetravel.aspect; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.TravelErrorCode; +import withbeetravel.repository.TravelMemberRepository; +import withbeetravel.repository.TravelRepository; +import withbeetravel.security.UserAuthorizationUtil; + +/** + * ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „ Travel์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์ฆ + */ + +@Aspect +@Component +@RequiredArgsConstructor +public class TravelAccessAspect { + + private final TravelRepository travelRepository; + private final TravelMemberRepository travelMemberRepository; + + @Before("@annotation(checkTravelAccess)") // @CheckTravelAccess๊ฐ€ ๋ถ™์€ ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „์— ์‹คํ–‰ + public void checkAccess(JoinPoint joinPoint, CheckTravelAccess checkTravelAccess) { + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + // @CheckTravelAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์†Œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ travelId ์ถ”์ถœ + Long travelId = getTravelIdFromArgs(joinPoint, checkTravelAccess.travelIdParam()); + + // travelId์— ํ•ด๋‹นํ•˜๋Š” travel์ด ์žˆ๋Š”์ง€ ํ™•์ธ(์—†๋‹ค๋ฉด ์˜ˆ์™ธ ๋˜์ง€๊ธฐ) + travelRepository.findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + + // ๊ถŒํ•œ ๊ฒ€์‚ฌ + travelMemberRepository.findByTravelIdAndUserId(travelId, userId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN)); + } + + private Long getTravelIdFromArgs(JoinPoint joinPoint, String travelIdParam) { + + // ๋ฉ”์†Œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์ด๋ฆ„์„ ๋งคํ•‘ํ•˜์—ฌ travelId๋ฅผ ์ฐพ์•„ ๋ฐ˜ํ™˜ + String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames(); + Object[] args = joinPoint.getArgs(); + + for (int i = 0; i < paramNames.length; i++) { + if(paramNames[i].equals(travelIdParam) && args[i] instanceof Long) { + return (Long) args[i]; + } + } + + // @CheckTravelAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์„œ๋“œ์—์„œ "travelId"๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ + throw new IllegalArgumentException("travelId ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/aspect/TravelAndSharedPaymentAccessAspect.java b/src/main/java/withbeetravel/aspect/TravelAndSharedPaymentAccessAspect.java new file mode 100644 index 00000000..27eacf54 --- /dev/null +++ b/src/main/java/withbeetravel/aspect/TravelAndSharedPaymentAccessAspect.java @@ -0,0 +1,75 @@ +package withbeetravel.aspect; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.PaymentErrorCode; +import withbeetravel.exception.error.TravelErrorCode; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelMemberRepository; +import withbeetravel.repository.TravelRepository; +import withbeetravel.security.UserAuthorizationUtil; + +/** + * ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „ Travel๊ณผ Shared payment์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์ฆ + */ + +@Aspect +@Component +@RequiredArgsConstructor +public class TravelAndSharedPaymentAccessAspect { + + private final TravelRepository travelRepository; + private final TravelMemberRepository travelMemberRepository; + private final SharedPaymentRepository sharedPaymentRepository; + + // @CheckTravelAndSharedPaymentAccess ๋ถ™์€ ๋ฉ”์†Œ๋“œ ์‹คํ–‰ ์ „์— ์‹คํ–‰ + @Before("@annotation(access)") + public void checkAccess(JoinPoint joinPoint, CheckTravelAndSharedPaymentAccess access) { + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + // @CheckTravelAndSharedPaymentAccess ๋ถ™์ธ ๋ฉ”์†Œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ travelId ์ถ”์ถœ + Long travelId = getIdFromArgs(joinPoint, access.travelIdParam()); + + // travelId์— ํ•ด๋‹นํ•˜๋Š” travel์ด ์žˆ๋Š”์ง€ ํ™•์ธ(์—†๋‹ค๋ฉด ์˜ˆ์™ธ ๋˜์ง€๊ธฐ) + travelRepository.findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + + // ๊ถŒํ•œ ๊ฒ€์‚ฌ + travelMemberRepository.findByTravelIdAndUserId(travelId, userId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN)); + + // @CheckTravelAndSharedPaymentAccess ๋ถ™์ธ ๋ฉ”์†Œ๋“œ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ sharedPaymentId ์ถ”์ถœ + Long sharedPaymentId = getIdFromArgs(joinPoint, access.sharedPaymentIdParam()); + + // sharedPaymentId์— ํ•ด๋‹นํ•˜๋Š” shared payment๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ(์—†๋‹ค๋ฉด ์˜ˆ์™ธ ๋˜์ง€๊ธฐ) + sharedPaymentRepository.findById(sharedPaymentId) + .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND)); + + // ๊ถŒํ•œ ๊ฒ€์‚ฌ + sharedPaymentRepository.findByIdAndTravelId(sharedPaymentId, travelId) + .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_ACCESS_FORBIDDEN)); + } + + private Long getIdFromArgs(JoinPoint joinPoint, String idParam) { + + // ๋ฉ”์†Œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์ด๋ฆ„์„ ๋งคํ•‘ํ•˜์—ฌ travelId๋ฅผ ์ฐพ์•„ ๋ฐ˜ํ™˜ + String[] paramNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames(); + Object[] args = joinPoint.getArgs(); + + + for (int i = 0; i < paramNames.length; i++) { + if(paramNames[i].equals(idParam) && args[i] instanceof Long) { + return (Long) args[i]; + } + } + + // @CheckTravelAccess๋ฅผ ๋ถ™์ธ ๋ฉ”์„œ๋“œ์—์„œ "travelId"๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ + throw new IllegalArgumentException(idParam + " ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/config/CorsConfig.java b/src/main/java/withbeetravel/config/CorsConfig.java new file mode 100644 index 00000000..ad3637c3 --- /dev/null +++ b/src/main/java/withbeetravel/config/CorsConfig.java @@ -0,0 +1,17 @@ +package withbeetravel.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry){ + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000", "http://192.168.0.5:3000", "http://54.180.164.254:8080","http://54.180.164.254", + "http://localhost:3001", "https://withbee-travel.vercel.app", "https://www.withbee.site") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowCredentials(true); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/config/OpenAIConfig.java b/src/main/java/withbeetravel/config/OpenAIConfig.java new file mode 100644 index 00000000..fa8c14fd --- /dev/null +++ b/src/main/java/withbeetravel/config/OpenAIConfig.java @@ -0,0 +1,22 @@ +package withbeetravel.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "openai") +@Getter +@Setter +public class OpenAIConfig { + @Value("${openai.api-key}") + private String apiKey; + + @Value("${openai.model}") + private String model; + + @Value("${openai.endpoint}") + private String endpoint; +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/config/PasswordEncoderConfig.java b/src/main/java/withbeetravel/config/PasswordEncoderConfig.java new file mode 100644 index 00000000..84f9ad93 --- /dev/null +++ b/src/main/java/withbeetravel/config/PasswordEncoderConfig.java @@ -0,0 +1,13 @@ +package withbeetravel.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/withbeetravel/config/S3Config.java b/src/main/java/withbeetravel/config/S3Config.java new file mode 100644 index 00000000..6c2afa28 --- /dev/null +++ b/src/main/java/withbeetravel/config/S3Config.java @@ -0,0 +1,35 @@ +package withbeetravel.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +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 // ์„ค์ • ํŒŒ์ผ์„ ์ฝ๊ธฐ ์œ„ํ•œ annotation +public class S3Config { + + // S3 + @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 AmazonS3 s3Client() { + + AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} diff --git a/src/main/java/withbeetravel/config/SecurityConfig.java b/src/main/java/withbeetravel/config/SecurityConfig.java index f29d8079..20f25d40 100644 --- a/src/main/java/withbeetravel/config/SecurityConfig.java +++ b/src/main/java/withbeetravel/config/SecurityConfig.java @@ -1,23 +1,60 @@ package withbeetravel.config; +import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import withbeetravel.jwt.JwtAuthFilter; +import withbeetravel.jwt.JwtUtil; +import withbeetravel.service.auth.CustomUserDetailsService; @Configuration @EnableWebSecurity +@AllArgsConstructor public class SecurityConfig{ + private final CustomUserDetailsService customUserDetailsService; + private final JwtUtil jwtUtil; + private static final String[] AUTH_WHITELIST = { + "/api/auth/login", + "/api/auth/join", + "/swagger-ui/**", + "/", + "/api/auth/reissue", + "/api/auth/check-time"}; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - // ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์ „๊นŒ์ง€ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ - http - .authorizeHttpRequests((requests) -> - requests.requestMatchers("/**").permitAll()); + // CSRF, CORS + http.csrf((csrf) -> csrf.disable()); + http.cors((Customizer.withDefaults())); + + // ์„ธ์…˜ ๊ด€๋ฆฌ ์ƒํƒœ ์—†์Œ์œผ๋กœ ๊ตฌ์„ฑ, Spring Securtiy๊ฐ€ ์„ธ์…˜ ์ƒ์„ฑ or ์‚ฌ์šฉ x + http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)); + + // FormLogin, BasicHttp, logout ๋น„ํ™œ์„ฑํ™” + http.formLogin(AbstractHttpConfigurer::disable); + http.httpBasic(AbstractHttpConfigurer::disable); + http.logout(AbstractHttpConfigurer::disable); + + // JwtAuthFilter๋ฅผ UsernamePasswordAuthenticationFilter ์•ž์— ์ถ”๊ฐ€ + http.addFilterBefore(new JwtAuthFilter(customUserDetailsService, jwtUtil), + UsernamePasswordAuthenticationFilter.class); + + // ๊ถŒํ•œ ๊ทœ์น™ ์ž‘์„ฑ + http.authorizeHttpRequests((authorize) -> + authorize.requestMatchers(AUTH_WHITELIST).permitAll() + .requestMatchers("/api/admin/**").hasRole("ADMIN") + .anyRequest().authenticated()); return http.build(); } -} +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/config/SwaggerConfig.java b/src/main/java/withbeetravel/config/SwaggerConfig.java new file mode 100644 index 00000000..02475a1a --- /dev/null +++ b/src/main/java/withbeetravel/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package withbeetravel.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration // ์„ค์ • ํŒŒ์ผ์„ ์ฝ๊ธฐ ์œ„ํ•œ annotation +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + + // Swagger์—์„œ ๋ณด์—ฌ์ค„ API ์ •๋ณด ์„ค์ • + Info info = new Info() + .version("1.0.0") // ์„œ๋น„์Šค ๋ฒ„์ „ + .title("WithBee Travel") // ํƒ€์ดํ‹€ + .description("WithBee Travel๐Ÿ๐Ÿ›ซ Project API"); // ์„ค๋ช… + + return new OpenAPI().info(info); + } +} diff --git a/src/main/java/withbeetravel/config/TaskSchedulerConfig.java b/src/main/java/withbeetravel/config/TaskSchedulerConfig.java new file mode 100644 index 00000000..c2af0251 --- /dev/null +++ b/src/main/java/withbeetravel/config/TaskSchedulerConfig.java @@ -0,0 +1,22 @@ +package withbeetravel.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +@Primary +public class TaskSchedulerConfig { + private static final int POOL_SIZE = 3; + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(POOL_SIZE); + scheduler.setThreadNamePrefix("SettlementScheduler-"); + scheduler.initialize(); + return scheduler; + } +} diff --git a/src/main/java/withbeetravel/controller/admin/AdminController.java b/src/main/java/withbeetravel/controller/admin/AdminController.java new file mode 100644 index 00000000..51beb5f5 --- /dev/null +++ b/src/main/java/withbeetravel/controller/admin/AdminController.java @@ -0,0 +1,81 @@ +package withbeetravel.controller.admin; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import withbeetravel.dto.request.admin.TravelAdminRequest; +import withbeetravel.dto.request.admin.UserRequest; +import withbeetravel.dto.response.admin.DashboardResponse; +import withbeetravel.dto.request.admin.LoginLogRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.admin.LoginLogResponse; +import withbeetravel.dto.response.admin.TravelAdminResponse; +import withbeetravel.dto.response.admin.UserResponse; +import withbeetravel.service.admin.AdminService; + +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +public class AdminController { + + private final AdminService adminService; + + @GetMapping + public SuccessResponse showDashboard(){ + return SuccessResponse.of( + HttpStatus.OK.value(), + "๋กœ๊ทธ์ธ, ์œ ์ €, ์—ฌํ–‰ ์ˆ˜ ์กฐํšŒ ์„ฑ๊ณต", + adminService.getLoginAttempts() + ); + } + + @GetMapping("/logs/all") + public SuccessResponse> showAllLoginHistories( + @RequestParam Long userId, + @RequestParam int page, + @RequestParam int size, + @RequestParam(required = false) String loginLogType + ){ + LoginLogRequest loginLogRequest = new LoginLogRequest(userId, page-1, size,loginLogType); + return SuccessResponse.of( + HttpStatus.OK.value(), + "userId์˜ ์ „์ฒด ๋กœ๊ทธ์ธ ๋กœ๊ทธ ์กฐํšŒ ์„ฑ๊ณต", + adminService.showAllLoginHistories(loginLogRequest) + ); + } + + @GetMapping("/users") + public SuccessResponse> showUsers( + @RequestParam int page, + @RequestParam int size, + @RequestParam(required = false) String name + ) { + UserRequest userRequest = UserRequest.builder().name(name). + page(page).size(size).build(); + return SuccessResponse.of( + HttpStatus.OK.value(), + "์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต", + adminService.showUsers(userRequest) + ); + } + + @GetMapping("/travels") + public SuccessResponse> showTravels( + @RequestParam int page, + @RequestParam int size, + @RequestParam(required = false) Long userId + ){ + TravelAdminRequest travelAdminRequest = TravelAdminRequest.builder() + .page(page).size(size).userId(userId).build(); + + return SuccessResponse.of( + HttpStatus.OK.value(), + userId == null ? "์ „์ฒด ์—ฌํ–‰ ์กฐํšŒ ์„ฑ๊ณต" : "์‚ฌ์šฉ์ž์˜ ์—ฌํ–‰ ์กฐํšŒ ์„ฑ๊ณต", + adminService.showTravels(travelAdminRequest) + ); + + } + + +} diff --git a/src/main/java/withbeetravel/controller/auth/AuthController.java b/src/main/java/withbeetravel/controller/auth/AuthController.java new file mode 100644 index 00000000..cd9130f1 --- /dev/null +++ b/src/main/java/withbeetravel/controller/auth/AuthController.java @@ -0,0 +1,70 @@ +package withbeetravel.controller.auth; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import withbeetravel.controller.auth.docs.AuthControllerDocs; +import withbeetravel.dto.request.auth.SignInRequest; +import withbeetravel.dto.request.auth.SignUpRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.auth.*; +import withbeetravel.security.CookieUtil; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.auth.AuthService; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController implements AuthControllerDocs { + + private final AuthService authService; + private final CookieUtil cookieUtil; + + @PostMapping("/join") + public SuccessResponse register(@RequestBody @Valid SignUpRequest signUpRequest) { + authService.signUp(signUpRequest); + return SuccessResponse.of(HttpStatus.CREATED.value(), "ํšŒ์› ๊ฐ€์ž… ์™„๋ฃŒ"); + } + + @PostMapping("/login") + public ResponseEntity> login(@RequestBody @Valid SignInRequest signInRequest) { + SignInResponse signInResponse = authService.login(signInRequest); + return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, String.valueOf(cookieUtil.createHttpOnlyCookie(signInResponse.getRefreshToken()))) + .body(SuccessResponse.of(HttpStatus.OK.value(), "๋กœ๊ทธ์ธ ์„ฑ๊ณต", signInResponse.getUserAuthResponse())); + } + + @PostMapping("/reissue") + public ResponseEntity> reissue(@RequestHeader("refreshToken") String refreshToken) { + ReissueResponse reissueResponse = authService.reissue(refreshToken); + + return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, + String.valueOf(cookieUtil.createHttpOnlyCookie(reissueResponse.getRefreshToken()))) + .body(SuccessResponse.of(HttpStatus.OK.value(), "ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต", reissueResponse.getAccessTokenResponse())); + } + + @GetMapping("/check-time") + public SuccessResponse checkTokenTime(@RequestHeader("token") String token) { + ExpirationResponse expirationResponse = authService.checkExpirationTime(token); + return SuccessResponse.of(HttpStatus.OK.value(), "ํ† ํฐ ๋งŒ๋ฃŒ์‹œ๊ฐ„ ์กฐํšŒ ์„ฑ๊ณต", expirationResponse); + } + + @PostMapping("/logout") + public SuccessResponse logout(@RequestHeader("refreshToken") String refreshToken) { + authService.logout(refreshToken); + return SuccessResponse.of(HttpStatus.OK.value(), "๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต"); + } + + @Override + @GetMapping("/mypage") + public SuccessResponse getMyPageInfo() { + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + MyPageResponse myPageInfo = authService.getMyPageInfo(userId); + return SuccessResponse.of(HttpStatus.OK.value(), "๋งˆ์ดํŽ˜์ด์ง€ ์ •๋ณด์ž…๋‹ˆ๋‹ค.", myPageInfo); + } +} + diff --git a/src/main/java/withbeetravel/controller/auth/docs/AuthControllerDocs.java b/src/main/java/withbeetravel/controller/auth/docs/AuthControllerDocs.java new file mode 100644 index 00000000..24c3afda --- /dev/null +++ b/src/main/java/withbeetravel/controller/auth/docs/AuthControllerDocs.java @@ -0,0 +1,27 @@ +package withbeetravel.controller.auth.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.auth.MyPageResponse; + +@Tag(name = "์ธ์ฆ API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface AuthControllerDocs { + + @Operation( + summary = "๋งˆ์ดํŽ˜์ด์ง€ ์ดˆ๊ธฐ ์ •๋ณด API", + description = "๋งˆ์ดํŽ˜์ด์ง€์— ํ•„์š”ํ•œ ์ดˆ๊ธฐ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "์ธ์ฆ"} + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "๋งˆ์ดํŽ˜์ด์ง€ ์ •๋ณด์ž…๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = MyPageResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "AUTH-017", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse getMyPageInfo(); +} diff --git a/src/main/java/withbeetravel/controller/banking/AccountController.java b/src/main/java/withbeetravel/controller/banking/AccountController.java new file mode 100644 index 00000000..32e3f05c --- /dev/null +++ b/src/main/java/withbeetravel/controller/banking/AccountController.java @@ -0,0 +1,134 @@ +package withbeetravel.controller.banking; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import withbeetravel.aspect.CheckBankingAccess; +import withbeetravel.dto.request.account.*; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.account.AccountConnectedWibeeResponse; +import withbeetravel.dto.response.account.AccountOwnerNameResponse; +import withbeetravel.dto.response.account.AccountResponse; +import withbeetravel.dto.response.account.HistoryResponse; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.banking.AccountService; +import withbeetravel.service.banking.HistoryService; + +import java.util.List; + +@RestController +@RequestMapping("/api/accounts") +@RequiredArgsConstructor +public class AccountController { + + private final AccountService accountService; + + private final HistoryService historyService; + + + + @GetMapping() + public SuccessResponse> showAllAccount(){ + Long userId = UserAuthorizationUtil.getLoginUserId(); + return SuccessResponse.of( + HttpStatus.OK.value(), + "์ „์ฒด ๊ณ„์ขŒ ์กฐํšŒ ์™„๋ฃŒ", + accountService.showAll(userId) + ); + } + + @GetMapping("/{accountId}/info") + @CheckBankingAccess(accountIdParam = "accountId") // AOP๋กœ ๊ถŒํ•œ ๊ฒ€์ฆ + public SuccessResponse accountInfo(@PathVariable Long accountId){ + return SuccessResponse.of( + HttpStatus.OK.value(), + "accountId๋กœ ๊ณ„์ขŒ ์กฐํšŒ ์„ฑ๊ณต", + accountService.accountInfo(accountId) + ); + } + + @GetMapping("/{accountId}") + @CheckBankingAccess(accountIdParam = "accountId") // AOP๋กœ ๊ถŒํ•œ ๊ฒ€์ฆ + public SuccessResponse> showAllHistories(@PathVariable Long accountId){ + return SuccessResponse.of( + HttpStatus.OK.value(), + "๊ณ„์ขŒ ๊ฑฐ๋ž˜๋‚ด์—ญ ์กฐํšŒ ์„ฑ๊ณต", + historyService.showAll(accountId) + ); + } + + @PostMapping() + public SuccessResponse createAccount(@RequestBody CreateAccountRequest createAccountRequest){ + + Long userId = UserAuthorizationUtil.getLoginUserId(); + // ๊ณ„์ขŒ ์ƒ์„ฑ + return SuccessResponse.of( + HttpStatus.CREATED.value(), + "๊ณ„์ขŒ ์ƒ์„ฑ ์™„๋ฃŒ", + accountService.createAccount(userId,createAccountRequest) + );//1L ๋ถ€๋ถ„์€ ๋‚˜์ค‘์— ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ๋˜๋ฉด ์œ ์ € ๋ณ„๋กœ ๋ฐ”๋€” ์˜ˆ์ • + } + + @PostMapping("{accountId}/transfer") + //@CheckBankingAccess(accountIdParam = "accountId") // AOP๋กœ ๊ถŒํ•œ ๊ฒ€์ฆ + public SuccessResponse transfer(@RequestBody TransferRequest transferRequest){ + + accountService.transfer(transferRequest.getAccountId(), + transferRequest.getAccountNumber(), + transferRequest.getAmount(), + transferRequest.getRqspeNm()); + + return SuccessResponse.of( + HttpStatus.ACCEPTED.value(), + "์†ก๊ธˆ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + ); + } + + @PostMapping("{accountId}/deposit") + @CheckBankingAccess(accountIdParam = "accountId") // AOP๋กœ ๊ถŒํ•œ ๊ฒ€์ฆ + public SuccessResponse deposit( + @RequestBody DepositRequest depositRequest, + @PathVariable Long accountId) { + + // ์ž…๊ธˆ ์ฒ˜๋ฆฌ + accountService.deposit(accountId, depositRequest.getAmount(), depositRequest.getRqspeNm()); + + // SuccessResponse ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ + return SuccessResponse.of( + HttpStatus.ACCEPTED.value(), // ์ƒํƒœ ์ฝ”๋“œ 202 + "์ž…๊ธˆ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." // ๋ฉ”์‹œ์ง€ + ); + } + + @PostMapping("/verify") + public SuccessResponse verifyAccount(@RequestBody AccountNumberRequest accountNumberRequest){ + + accountService.verifyAccount(accountNumberRequest.getAccountNumber()); + + return SuccessResponse.of( + HttpStatus.OK.value(), + "๊ณ„์ขŒ ๋ฒˆํ˜ธ ์กด์žฌ ํ™•์ธ ์™„๋ฃŒ" + ); + + } + + @PostMapping("/find-user") + public SuccessResponse findUserNameByAccountNumber(@RequestBody AccountNumberRequest accountNumberRequest) { + return SuccessResponse.of( + HttpStatus.OK.value(), + "์ฐพ์€ ๊ณ„์ขŒ ์ฃผ์ธ ์ด๋ฆ„", + accountService.findUserNameByAccountNumber(accountNumberRequest.getAccountNumber()) + ); + } + + @GetMapping("/{accountId}/check-wibee") + public SuccessResponse connectedWibee( + @PathVariable Long accountId){ + return SuccessResponse.of( + HttpStatus.OK.value(), + "์œ„๋น„ ์นด๋“œ ์—ฐ๊ฒฐ ์—ฌ๋ถ€ ํ™•์ธ ์™„๋ฃŒ", + accountService.connectedWibee(accountId) + ); + } + +} diff --git a/src/main/java/withbeetravel/controller/banking/HistoryController.java b/src/main/java/withbeetravel/controller/banking/HistoryController.java new file mode 100644 index 00000000..9448e4be --- /dev/null +++ b/src/main/java/withbeetravel/controller/banking/HistoryController.java @@ -0,0 +1,51 @@ +package withbeetravel.controller.banking; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import withbeetravel.aspect.CheckBankingAccess; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.controller.banking.docs.HistoryControllerDocs; +import withbeetravel.dto.request.account.HistoryRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.account.WibeeCardHistoryListResponse; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.banking.HistoryService; + +@RestController +@RequestMapping("/api/accounts") +@RequiredArgsConstructor +public class HistoryController implements HistoryControllerDocs { + + private final HistoryService historyService; + + @Override + @CheckBankingAccess + @PostMapping("/{accountId}/payment") + public SuccessResponse addPayment( + @PathVariable Long accountId, + @RequestBody HistoryRequest historyRequest + ){ + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + historyService.addHistory(userId, accountId,historyRequest); + return SuccessResponse.of( + HttpStatus.CREATED.value(), + "๊ฒฐ์ œ ๋‚ด์—ญ ๋“ฑ๋ก ์™„๋ฃŒ" + ); + } + + @Override + @GetMapping("/wibeeCardHistory") + public SuccessResponse getWibeeCardHistory( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate + ) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + + WibeeCardHistoryListResponse response = historyService.getWibeeCardHistory(userId, startDate, endDate); + + return SuccessResponse.of(HttpStatus.OK.value(), "์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค.", response); + } +} diff --git a/src/main/java/withbeetravel/controller/banking/VerifyController.java b/src/main/java/withbeetravel/controller/banking/VerifyController.java new file mode 100644 index 00000000..c7dc7627 --- /dev/null +++ b/src/main/java/withbeetravel/controller/banking/VerifyController.java @@ -0,0 +1,34 @@ +package withbeetravel.controller.banking; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import withbeetravel.dto.request.account.PinNumberRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.account.PinNumberResponse; +import withbeetravel.service.banking.VerifyService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/verify") +public class VerifyController { + + private final VerifyService verifyService; + + @PostMapping("/pin-number") + public SuccessResponse verifyPin( + @RequestBody PinNumberRequest pinNumberRequest){ + verifyService.verifyPin(pinNumberRequest.getPinNumber()); + + return SuccessResponse.of(HttpStatus.OK.value(), "ํ•€๋ฒˆํ˜ธ ๊ฒ€์ฆ ์™„๋ฃŒ"); + } + + @GetMapping("/user-state") + public SuccessResponse verifyUser(){ + return SuccessResponse.of( + HttpStatus.OK.value(), + "์œ ์ €๊ฐ€ ์ž ๊ธˆ์ƒํƒœ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.", + verifyService.verifyUser() + ); + } +} diff --git a/src/main/java/withbeetravel/controller/banking/docs/HistoryControllerDocs.java b/src/main/java/withbeetravel/controller/banking/docs/HistoryControllerDocs.java new file mode 100644 index 00000000..0e9a4e93 --- /dev/null +++ b/src/main/java/withbeetravel/controller/banking/docs/HistoryControllerDocs.java @@ -0,0 +1,76 @@ +package withbeetravel.controller.banking.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import withbeetravel.dto.request.account.HistoryRequest; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.account.WibeeCardHistoryListResponse; + +@Tag(name = "์ฝ”์–ด๋ฑ…ํ‚น API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface HistoryControllerDocs { + + @Operation( + summary = "๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ํ•˜๊ธฐ API", + description = "๊ณ„์ขŒ์— ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "์ฝ”์–ด๋ฑ…ํ‚น"}, + parameters = { + @Parameter( + name = "accountId", + description = "๊ณ„์ขŒ ID", + in = ParameterIn.DEFAULT, + example = "1234" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "๊ฒฐ์ œ ๋‚ด์—ญ ๋“ฑ๋ก ์™„๋ฃŒ", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "400", description = "BANKING-01", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "BANKING-004\nBANKING-006", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "BANKING-002", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse addPayment( + @PathVariable Long accountId, + @RequestBody HistoryRequest historyRequest + ); + + @Operation( + summary = "์œ„๋น„ ํŠธ๋ž˜๋ธ” ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ API", + description = "์œ„๋น„ ํŠธ๋ž˜๋ธ” ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "์ฝ”์–ด๋ฑ…ํ‚น"}, + parameters = { + @Parameter( + name = "startDate", + description = "ํ•„ํ„ฐ๋ง ์‹œ์ž‘ ๋ฒ”์œ„", + in = ParameterIn.QUERY, + example = "2024-11-26" + ), + @Parameter( + name = "endDate", + description = "ํ•„ํ„ฐ๋ง ๋ ๋ฒ”์œ„", + in = ParameterIn.QUERY, + example = "2024-11-28" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = WibeeCardHistoryListResponse.class))), + @ApiResponse(responseCode = "400", description = "VALIDATION-003", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "BANKING-003\nBANKING-006", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "AUTH-017", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse getWibeeCardHistory( + String startDate, + String endDate + ); +} diff --git a/src/main/java/withbeetravel/controller/payment/SharedPaymentController.java b/src/main/java/withbeetravel/controller/payment/SharedPaymentController.java new file mode 100644 index 00000000..15adb3d8 --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/SharedPaymentController.java @@ -0,0 +1,42 @@ +package withbeetravel.controller.payment; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.aspect.PaymentValidation; +import withbeetravel.domain.SharedPayment; +import withbeetravel.dto.request.payment.SharedPaymentSearchRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.payment.SharedPaymentParticipatingMemberResponse; +import withbeetravel.dto.response.payment.SharedPaymentResponse; +import withbeetravel.service.payment.SharedPaymentService; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/travels/{travelId}/payments") +public class SharedPaymentController { + + private final SharedPaymentService sharedPaymentService; + + @CheckTravelAccess + @PaymentValidation + @GetMapping + public SuccessResponse> getSharedPayments( + @PathVariable Long travelId, + @Valid @ModelAttribute SharedPaymentSearchRequest condition + ) { + Page payments = sharedPaymentService.getSharedPayments(travelId, condition); + Map> participatingMembersMap = sharedPaymentService.getParticipatingMembersMap(payments); + + return SuccessResponse.of(200, "๋ชจ๋“  ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์กฐํšŒ ์„ฑ๊ณต", SharedPaymentResponse.of( + payments, + payments.getContent().get(0).getTravel().getTravelMembers().size(), + participatingMembersMap + )); + } +} diff --git a/src/main/java/withbeetravel/controller/payment/SharedPaymentParticipantController.java b/src/main/java/withbeetravel/controller/payment/SharedPaymentParticipantController.java new file mode 100644 index 00000000..471d04f2 --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/SharedPaymentParticipantController.java @@ -0,0 +1,33 @@ +package withbeetravel.controller.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import withbeetravel.aspect.CheckTravelAndSharedPaymentAccess; +import withbeetravel.controller.payment.docs.SharedPaymentParticipantControllerDocs; +import withbeetravel.dto.request.payment.SharedPaymentParticipateRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.service.payment.SharedPaymentParticipantService; + +@RestController +@RequestMapping("/api/travels/{travelId}/payments/{sharedPaymentId}/participants") +@RequiredArgsConstructor +public class SharedPaymentParticipantController implements SharedPaymentParticipantControllerDocs { + + private final SharedPaymentParticipantService sharedPaymentParticipantService; + + @Override + @PatchMapping + @CheckTravelAndSharedPaymentAccess + public SuccessResponse updateParticipantMembers( + @PathVariable Long travelId, + @PathVariable Long sharedPaymentId, + @RequestBody SharedPaymentParticipateRequest sharedPaymentParticipateRequest + ) { + + sharedPaymentParticipantService + .updateParticipantMembers(travelId, sharedPaymentId, sharedPaymentParticipateRequest); + + return SuccessResponse.of(HttpStatus.OK.value(), "์ •์‚ฐ์ธ์› ๋ณ€๊ฒฝ ์„ฑ๊ณต"); + } +} diff --git a/src/main/java/withbeetravel/controller/payment/SharedPaymentRecordController.java b/src/main/java/withbeetravel/controller/payment/SharedPaymentRecordController.java new file mode 100644 index 00000000..d77c5782 --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/SharedPaymentRecordController.java @@ -0,0 +1,45 @@ +package withbeetravel.controller.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.aspect.CheckTravelAndSharedPaymentAccess; +import withbeetravel.controller.payment.docs.SharedPaymentRecordControllerDocs; +import withbeetravel.dto.response.payment.SharedPaymentRecordResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.service.payment.SharedPaymentRecordService; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/travels/{travelId}/payments") +public class SharedPaymentRecordController implements SharedPaymentRecordControllerDocs { + + private final SharedPaymentRecordService sharedPaymentService; + + @Override + @CheckTravelAndSharedPaymentAccess + @PatchMapping(value = "/{sharedPaymentId}/records", consumes = "multipart/form-data") + public SuccessResponse addAndUpdatePaymentRecord( + @PathVariable Long travelId, + @PathVariable Long sharedPaymentId, + @RequestPart(value = "paymentImage") MultipartFile paymentImage, + @RequestParam(value = "paymentComment", required = false) String paymentComment, + @RequestParam(value = "isMainImage", defaultValue = "false") boolean isMainImage + ) { + + sharedPaymentService.addAndUpdatePaymentRecord(travelId, sharedPaymentId, paymentImage, paymentComment, isMainImage); + return SuccessResponse.of(HttpStatus.OK.value(), "์ด๋ฏธ์ง€ ๋ฐ ๋ฌธ๊ตฌ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค."); + } + + @Override + @CheckTravelAndSharedPaymentAccess + @GetMapping("/{sharedPaymentId}/records") + public SuccessResponse getSharedPaymentRecord( + @PathVariable Long travelId, + @PathVariable Long sharedPaymentId + ) { + SharedPaymentRecordResponse response = sharedPaymentService.getSharedPaymentRecord(sharedPaymentId); + return SuccessResponse.of(HttpStatus.OK.value(), "์—ฌํ–‰ ๊ธฐ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์„ฑ๊ณต", response); + } +} diff --git a/src/main/java/withbeetravel/controller/payment/SharedPaymentRegisterController.java b/src/main/java/withbeetravel/controller/payment/SharedPaymentRegisterController.java new file mode 100644 index 00000000..951caff7 --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/SharedPaymentRegisterController.java @@ -0,0 +1,105 @@ +package withbeetravel.controller.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.aspect.CheckTravelAndSharedPaymentAccess; +import withbeetravel.controller.payment.docs.SharedPaymentRegisterControllerDocs; +import withbeetravel.dto.request.payment.SharedPaymentWibeeCardRegisterRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.payment.CurrencyUnitResponse; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.payment.SharedPaymentRegisterService; + +@RestController +@RequestMapping("/api/travels/{travelId}/payments") +@RequiredArgsConstructor +public class SharedPaymentRegisterController implements SharedPaymentRegisterControllerDocs { + + private final SharedPaymentRegisterService sharedPaymentRegisterService; + + @Override + @CheckTravelAccess + @PostMapping(value = "/manual", consumes = "multipart/form-data") + public SuccessResponse addManualSharedPayment( + @PathVariable Long travelId, + @RequestParam(value = "paymentDate") String paymentDate, + @RequestParam(value = "storeName") String storeName, + @RequestParam(value = "paymentAmount") int paymentAmount, + @RequestParam(value = "foreignPaymentAmount", required = false) Double foreignPaymentAmount, + @RequestParam(value = "currencyUnit")String currencyUnit, + @RequestParam(value = "exchangeRate", required = false) Double exchangeRate, + @RequestPart(value = "paymentImage") MultipartFile paymentImage, + @RequestParam(value = "paymentComment", required = false) String paymentComment, + @RequestParam(value = "isMainImage", defaultValue = "false") boolean isMainImage + ) { + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + sharedPaymentRegisterService.addManualSharedPayment( + userId, travelId, paymentDate, storeName, paymentAmount, + foreignPaymentAmount, currencyUnit, exchangeRate, paymentImage, paymentComment, + isMainImage + ); + + return SuccessResponse.of(HttpStatus.OK.value(), "๊ฒฐ์ œ ๋‚ด์—ญ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Override + @CheckTravelAndSharedPaymentAccess + @PatchMapping(value = "/{sharedPaymentId}", consumes = "multipart/form-data") + public SuccessResponse updateManualSharedPayment( + @PathVariable Long travelId, + @PathVariable Long sharedPaymentId, + @RequestParam(value = "paymentDate") String paymentDate, + @RequestParam(value = "storeName") String storeName, + @RequestParam(value = "paymentAmount") int paymentAmount, + @RequestParam(value = "foreignPaymentAmount", required = false) Double foreignPaymentAmount, + @RequestParam(value = "currencyUnit")String currencyUnit, + @RequestParam(value = "exchangeRate", required = false) Double exchangeRate, + @RequestPart(value = "paymentImage") MultipartFile paymentImage, + @RequestParam(value = "paymentComment", required = false) String paymentComment, + @RequestParam(value = "isMainImage", defaultValue = "false") boolean isMainImage + ) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + + sharedPaymentRegisterService.updateManualSharedPayment( + userId, travelId, sharedPaymentId, paymentDate, storeName, + paymentAmount, foreignPaymentAmount, currencyUnit, exchangeRate, paymentImage, + paymentComment, isMainImage + ); + + return SuccessResponse.of(HttpStatus.OK.value(), "๊ฒฐ์ œ ๋‚ด์—ญ ์ •๋ณด๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Override + @CheckTravelAccess + @PostMapping("/manual-wibee-card") + public SuccessResponse addWibeeCardSharedPayment( + @PathVariable Long travelId, + @RequestBody SharedPaymentWibeeCardRegisterRequest sharedPaymentWibeeCardRegisterRequest + ) { + + Long userId = UserAuthorizationUtil.getLoginUserId(); + + sharedPaymentRegisterService.addWibeeCardSharedPayment( + userId, travelId, sharedPaymentWibeeCardRegisterRequest + ); + + return SuccessResponse.of(HttpStatus.OK.value(), "๊ฒฐ์ œ ๋‚ด์—ญ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + } + + @Override + @CheckTravelAccess + @GetMapping("/currency-unit") + public SuccessResponse getCurrencyUnitOptions( + @PathVariable Long travelId + ) { + + CurrencyUnitResponse response = sharedPaymentRegisterService.getCurrencyUnitOptions(travelId); + + return SuccessResponse.of(HttpStatus.OK.value(), "ํ†ตํ™” ์ฝ”๋“œ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค.", response); + } +} diff --git a/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentParticipantControllerDocs.java b/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentParticipantControllerDocs.java new file mode 100644 index 00000000..f226928c --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentParticipantControllerDocs.java @@ -0,0 +1,51 @@ +package withbeetravel.controller.payment.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import withbeetravel.dto.request.payment.SharedPaymentParticipateRequest; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.dto.response.SuccessResponse; + +@Tag(name = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface SharedPaymentParticipantControllerDocs { + + @Operation( + summary = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ฐธ์—ฌ ์ธ์› ์ˆ˜์ • API", + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ฐธ์—ฌ ์ธ์›์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "sharedPaymentId", + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "์ด๋ฏธ์ง€ ๋ฐ ๋ฌธ๊ตฌ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002\nPAYMENT-003\nPAYMENT-004", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001\nPAYMENT-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + public SuccessResponse updateParticipantMembers( + Long travelId, + Long sharedPaymentId, + SharedPaymentParticipateRequest sharedPaymentParticipateRequest + ); + +} diff --git a/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentRecordControllerDocs.java b/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentRecordControllerDocs.java new file mode 100644 index 00000000..afd7ce92 --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentRecordControllerDocs.java @@ -0,0 +1,101 @@ +package withbeetravel.controller.payment.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.dto.response.payment.SharedPaymentRecordResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.ErrorResponse; + +@Tag(name = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface SharedPaymentRecordControllerDocs { + + @Operation( + summary = "์—ฌํ–‰ ๊ธฐ๋ก ์ถ”๊ฐ€/์ˆ˜์ •ํ•˜๊ธฐ", + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์— ๋Œ€ํ•ด ์ด๋ฏธ์ง€, ๋ฌธ๊ตฌ๋ฅผ ์ถ”๊ฐ€/์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "sharedPaymentId", + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "paymentImage", + description = "์ด๋ฏธ์ง€(nullable)", + required = true, + in = ParameterIn.DEFAULT, + example = "image.jpg", + allowEmptyValue = true + ), + @Parameter( + name = "paymentComment", + description = "๋ฌธ๊ตฌ", + in = ParameterIn.DEFAULT, + example = "์–ํ˜ธ๋ฐ•๊ณ ๊ตฌ๋งˆ~" + ), + @Parameter( + name = "isMainImage", + description = "๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์—ฌ๋ถ€", + in = ParameterIn.DEFAULT + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "์ด๋ฏธ์ง€ ๋ฐ ๋ฌธ๊ตฌ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002\nPAYMENT-003", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001\nPAYMENT-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "422", description = "VALIDATION-004", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + public SuccessResponse addAndUpdatePaymentRecord(Long travelId, Long sharedPaymentId, + MultipartFile paymentImage, String paymentComment, boolean isMainImage); + + @Operation( + summary = "SHARED PAYMENT ID์— ๋”ฐ๋ฅธ ์—ฌํ–‰ ๊ธฐ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", + description = "SHARED PAYMENT ID์— ๋”ฐ๋ฅธ ์—ฌํ–‰ ์‚ฌ์ง„, ๋ฌธ๊ตฌ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "sharedPaymentId", + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "์—ฌํ–‰ ๊ธฐ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์„ฑ๊ณต", content = @Content(schema = @Schema(implementation = SharedPaymentRecordResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002\nPAYMENT-003", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001\nPAYMENT-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + public SuccessResponse getSharedPaymentRecord( + Long travelId, + Long sharedPaymentId + ); + +} diff --git a/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentRegisterControllerDocs.java b/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentRegisterControllerDocs.java new file mode 100644 index 00000000..c5e8352e --- /dev/null +++ b/src/main/java/withbeetravel/controller/payment/docs/SharedPaymentRegisterControllerDocs.java @@ -0,0 +1,268 @@ +package withbeetravel.controller.payment.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.dto.request.payment.SharedPaymentWibeeCardRegisterRequest; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.payment.CurrencyUnitResponse; + +@Tag(name = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface SharedPaymentRegisterControllerDocs { + + @Operation( + summary = "์ง์ ‘ ๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ํ•˜๊ธฐ", + description = "ํ˜„๊ธˆ ์ง€์ถœ์ด๋‚˜ ์œ„๋น„ ํŠธ๋ž˜๋ธ” ์นด๋“œ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž๋Š” ์ง์ ‘ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "paymentDate", + description = "๊ฒฐ์ œ ์‹œ๊ฐ", + required = true, + in = ParameterIn.DEFAULT, + example = "2024-11-01 14:33" + ), + @Parameter( + name = "storeName", + description = "์ƒํ˜ธ๋ช…", + required = true, + in = ParameterIn.DEFAULT, + example = "๋œจ๊ฐœ๋œจ๊ฐœ" + ), + @Parameter( + name = "paymentAmount", + description = "๊ฒฐ์ œ ๊ธˆ์•ก(์›ํ™”)", + required = true, + in = ParameterIn.DEFAULT, + example = "8990" + ), + @Parameter( + name = "foreignPaymentAmount", + description = "๊ฒฐ์ œ ๊ธˆ์•ก(์™ธํ™”)", + in = ParameterIn.DEFAULT, + example = "1.25", + allowEmptyValue = true + ), + @Parameter( + name = "currencyUnit", + description = "ํ†ตํ™” ๋‹จ์œ„", + required = true, + in = ParameterIn.DEFAULT, + example = "USD" + ), + @Parameter( + name = "exchangeRate", + description = "ํ™˜์œจ", + in = ParameterIn.DEFAULT, + example = "1392.50", + allowEmptyValue = true + ), + @Parameter( + name = "paymentImage", + description = "์ด๋ฏธ์ง€(nullable)", + required = true, + in = ParameterIn.DEFAULT, + example = "image.jpg", + allowEmptyValue = true + ), + @Parameter( + name = "paymentComment", + description = "๋ฌธ๊ตฌ", + in = ParameterIn.DEFAULT, + example = "์–ํ˜ธ๋ฐ•๊ณ ๊ตฌ๋งˆ~" + ), + @Parameter( + name = "isMainImage", + description = "๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์—ฌ๋ถ€", + in = ParameterIn.DEFAULT + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "๊ฒฐ์ œ ๋‚ด์—ญ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "400", description = "VALIDATION-001\nVALIDATION-002\nVALIDATION-005", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse addManualSharedPayment( + Long travelId, + String paymentDate, + String storeName, + int paymentAmount, + Double foreignPaymentAmount, + String currencyUnit, + Double exchangeRate, + MultipartFile paymentImage, + String paymentComment, + boolean isMainImage + ); + + @Operation( + summary = "์ง์ ‘ ๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ํ•˜๊ธฐ", + description = "ํ˜„๊ธˆ ์ง€์ถœ์ด๋‚˜ ์œ„๋น„ ํŠธ๋ž˜๋ธ” ์นด๋“œ๊ฐ€ ์—†๋Š” ์‚ฌ์šฉ์ž๋Š” ์ง์ ‘ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "sharedPaymentId", + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "paymentDate", + description = "๊ฒฐ์ œ ์‹œ๊ฐ", + required = true, + in = ParameterIn.DEFAULT, + example = "2024-11-01 14:33" + ), + @Parameter( + name = "storeName", + description = "์ƒํ˜ธ๋ช…", + required = true, + in = ParameterIn.DEFAULT, + example = "๋œจ๊ฐœ๋œจ๊ฐœ" + ), + @Parameter( + name = "paymentAmount", + description = "๊ฒฐ์ œ ๊ธˆ์•ก(์›ํ™”)", + required = true, + in = ParameterIn.DEFAULT, + example = "8990" + ), + @Parameter( + name = "foreignPaymentAmount", + description = "๊ฒฐ์ œ ๊ธˆ์•ก(์™ธํ™”)", + in = ParameterIn.DEFAULT, + example = "1.25", + allowEmptyValue = true + ), + @Parameter( + name = "currencyUnit", + description = "ํ†ตํ™” ๋‹จ์œ„", + required = true, + in = ParameterIn.DEFAULT, + example = "USD" + ), + @Parameter( + name = "exchangeRate", + description = "ํ™˜์œจ", + in = ParameterIn.DEFAULT, + example = "1392.50", + allowEmptyValue = true + ), + @Parameter( + name = "paymentImage", + description = "์ด๋ฏธ์ง€(nullable)", + required = true, + in = ParameterIn.DEFAULT, + example = "image.jpg", + allowEmptyValue = true + ), + @Parameter( + name = "paymentComment", + description = "๋ฌธ๊ตฌ", + in = ParameterIn.DEFAULT, + example = "์–ํ˜ธ๋ฐ•๊ณ ๊ตฌ๋งˆ~" + ), + @Parameter( + name = "isMainImage", + description = "๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์—ฌ๋ถ€", + in = ParameterIn.DEFAULT + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "๊ฒฐ์ œ ๋‚ด์—ญ ์ •๋ณด๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "400", description = "VALIDATION-001\nVALIDATION-002\nVALIDATION-005", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002\nPAYMENT-002", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001\nPAYMENT-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse updateManualSharedPayment( + Long travelId, + Long sharedPaymentId, + String paymentDate, + String storeName, + int paymentAmount, + Double foreignPaymentAmount, + String currencyUnit, + Double exchangeRate, + MultipartFile paymentImage, + String paymentComment, + boolean isMainImage + ); + + @Operation( + summary = "์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์— ์ถ”๊ฐ€ํ•˜๊ธฐ", + description = "์—ฌํ–‰ ์ผ์ž ์ด์ „์— ๋ฐœ์ƒํ•œ ์œ„๋น„ ํŠธ๋ž˜๋ธ” ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "๊ฒฐ์ œ ๋‚ด์—ญ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "400", description = "VALIDATION-003\nBANKING-007", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002\nBANKING-003\nBANKING-006", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001\nBANKING-005", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse addWibeeCardSharedPayment( + Long travelId, + SharedPaymentWibeeCardRegisterRequest sharedPaymentWibeeCardRegisterRequest + + ); + + @Operation( + summary = "ํ†ตํ™” ์ฝ”๋“œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", + description = "๋ฐฉ๋ฌธ ๋‚˜๋ผ ํ†ตํ™”๋ฅผ ์šฐ์„ ์œผ๋กœ ํ†ตํ™” ์ฝ”๋“œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ", + tags = {"User Management", "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "ํ†ตํ™” ์ฝ”๋“œ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค.", content = @Content(schema = @Schema(implementation = CurrencyUnitResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse getCurrencyUnitOptions( + Long travelId + ); +} diff --git a/src/main/java/withbeetravel/controller/settlement/SettlementController.java b/src/main/java/withbeetravel/controller/settlement/SettlementController.java new file mode 100644 index 00000000..f620e6c8 --- /dev/null +++ b/src/main/java/withbeetravel/controller/settlement/SettlementController.java @@ -0,0 +1,49 @@ +package withbeetravel.controller.settlement; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.settlement.ShowSettlementDetailResponse; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.settlement.SettlementService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/travels/{travelId}/settlements") +public class SettlementController { + private final SettlementService settlementService; + + @GetMapping + @CheckTravelAccess + SuccessResponse getSettlementDetails(@PathVariable Long travelId) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + ShowSettlementDetailResponse showSettlementDetailResponse = settlementService.getSettlementDetails(userId, travelId); + return SuccessResponse.of(HttpStatus.OK.value(), "์„ธ๋ถ€ ์ง€์ถœ ๋‚ด์—ญ ์กฐํšŒ ์„ฑ๊ณต", showSettlementDetailResponse); + } + + @PostMapping + @CheckTravelAccess + SuccessResponse requestSettlement(@PathVariable Long travelId) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + settlementService.requestSettlement(userId, travelId); + return SuccessResponse.of(HttpStatus.OK.value(), "์ •์‚ฐ ์š”์ฒญ ์„ฑ๊ณต"); + } + + @PostMapping("/agreement") + @CheckTravelAccess + SuccessResponse agreeSettlement(@PathVariable Long travelId) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + String message = settlementService.agreeSettlement(userId, travelId); + return SuccessResponse.of(HttpStatus.OK.value(), message); + } + + @DeleteMapping + @CheckTravelAccess + SuccessResponse cancelSettlement(@PathVariable Long travelId) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + settlementService.cancelSettlement(userId, travelId); + return SuccessResponse.of(HttpStatus.OK.value(), "์ •์‚ฐ ์ทจ์†Œ ์„ฑ๊ณต"); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/controller/settlement/SettlementRequestLogController.java b/src/main/java/withbeetravel/controller/settlement/SettlementRequestLogController.java new file mode 100644 index 00000000..5044f5ed --- /dev/null +++ b/src/main/java/withbeetravel/controller/settlement/SettlementRequestLogController.java @@ -0,0 +1,51 @@ +package withbeetravel.controller.settlement; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.dto.request.settlementRequestLog.SettlementRequestLogDto; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.notification.NotificationService; +import withbeetravel.service.notification.SettlementRequestLogService; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/notifications") +public class SettlementRequestLogController { + + private final NotificationService notificationService; + private final SettlementRequestLogService settlementRequestLogService; + + @GetMapping + SuccessResponse> getNotifications() { + Long userId = UserAuthorizationUtil.getLoginUserId(); + List settlementRequestLogs = settlementRequestLogService.getSettlementRequestLogs(userId); + return SuccessResponse.of(HttpStatus.OK.value(), "์•Œ๋ฆผ์ฐฝ ์กฐํšŒ ์„ฑ๊ณต", settlementRequestLogs); + } + + /** + * streamNotifications : ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ๋ฐ›์•„ SSE ๊ตฌ๋…ํ•˜๋„๋ก ์—ฐ๊ฒฐ + * - SseEmitter๋ฅผ ๋ฐ˜ํ™˜ + */ + @GetMapping(value = "/stream", produces = "text/event-stream") + public ResponseEntity streamNotifications(@RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") + String lastEventId) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + + // ํ—ค๋” ์„ค์ • + HttpHeaders headers = new HttpHeaders(); + headers.add("Cache-Control", "no-cache"); // ์บ์‹œ ๋ฐฉ์ง€ + headers.add("X-Accel-Buffering", "no"); + + return new ResponseEntity<>(notificationService.subscribe(userId, lastEventId), headers, HttpStatus.OK); + } +} diff --git a/src/main/java/withbeetravel/controller/travel/HoneyCapsuleController.java b/src/main/java/withbeetravel/controller/travel/HoneyCapsuleController.java new file mode 100644 index 00000000..df3c987b --- /dev/null +++ b/src/main/java/withbeetravel/controller/travel/HoneyCapsuleController.java @@ -0,0 +1,33 @@ +package withbeetravel.controller.travel; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.controller.travel.docs.HoneyCapsuleControllerDocs; +import withbeetravel.dto.response.travel.HoneyCapsuleResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.service.travel.HoneyCapsuleService; + +import java.util.List; + +@RestController +@RequestMapping("/api/travels/{travelId}/honeycapsule") +@RequiredArgsConstructor +public class HoneyCapsuleController implements HoneyCapsuleControllerDocs { + + private final HoneyCapsuleService honeyCapsuleService; + + @Override + @CheckTravelAccess + @GetMapping + public SuccessResponse> getHoneyCapsuleList( + @PathVariable Long travelId + ) { + + return honeyCapsuleService.getHoneyCapsuleList(travelId); + } + +} diff --git a/src/main/java/withbeetravel/controller/travel/TravelController.java b/src/main/java/withbeetravel/controller/travel/TravelController.java new file mode 100644 index 00000000..2dae8c9f --- /dev/null +++ b/src/main/java/withbeetravel/controller/travel/TravelController.java @@ -0,0 +1,102 @@ +package withbeetravel.controller.travel; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.controller.travel.docs.TravelControllerDocs; +import withbeetravel.dto.request.account.CardCompletedRequest; +import withbeetravel.dto.request.travel.InviteCodeSignUpRequest; +import withbeetravel.dto.request.travel.TravelRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.account.AccountConnectedWibeeResponse; +import withbeetravel.dto.response.travel.TravelHomeResponse; +import withbeetravel.dto.response.travel.InviteCodeGetResponse; +import withbeetravel.dto.response.travel.InviteCodeSignUpResponse; +import withbeetravel.dto.response.travel.TravelResponse; +import withbeetravel.dto.response.travel.TravelListResponse; +import withbeetravel.security.UserAuthorizationUtil; +import withbeetravel.service.travel.TravelService; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/travels") +public class TravelController implements TravelControllerDocs { + + private final TravelService travelService; + + @CheckTravelAccess + @GetMapping("/{travelId}") + public SuccessResponse getTravel(@PathVariable Long travelId) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + + return SuccessResponse.of(200, "์—ฌํ–‰ ํ™ˆ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์„ฑ๊ณต", travelService.getTravel(travelId, userId)); + } + + @PostMapping + public SuccessResponse saveTravel(@RequestBody TravelRequest request) { + Long userId = UserAuthorizationUtil.getLoginUserId(); + TravelResponse travelResponse = travelService.saveTravel(request,userId); + return SuccessResponse.of(HttpStatus.OK.value(), "์—ฌํ–‰ ์ƒ์„ฑ ์„ฑ๊ณต",travelResponse); + } + + @CheckTravelAccess + @PatchMapping("/{travelId}") + public SuccessResponse editTravel(@PathVariable Long travelId, @RequestBody TravelRequest request) { + // ์—ฌํ–‰ ์ •๋ณด ์ˆ˜์ • + travelService.editTravel(request, travelId); + return SuccessResponse.of(HttpStatus.OK.value(), "์—ฌํ–‰ ํŽธ์ง‘ ์„ฑ๊ณต"); + } + + @PostMapping("/invite-code") + public SuccessResponse signUpTravel(@RequestBody InviteCodeSignUpRequest request){ + Long userId = UserAuthorizationUtil.getLoginUserId(); + InviteCodeSignUpResponse inviteCodeResponse = travelService.signUpTravel(request,userId); + return SuccessResponse.of(HttpStatus.OK.value(), "์—ฌํ–‰ ๊ฐ€์ž… ์„ฑ๊ณต", inviteCodeResponse); + } + + @GetMapping("/{travelId}/invite-code") + @CheckTravelAccess + public SuccessResponse getInviteCode(@PathVariable Long travelId){ + InviteCodeGetResponse inviteCodeGetReponse = travelService.getInviteCode(travelId); + return SuccessResponse.of(HttpStatus.OK.value(), "์ดˆ๋Œ€ ์ฝ”๋“œ ์กฐํšŒ ์„ฑ๊ณต", inviteCodeGetReponse); + } + + + @GetMapping + public SuccessResponse> getTravelList() { + Long userId = UserAuthorizationUtil.getLoginUserId(); + List travelListResponse = travelService.getTravelList(userId); + return SuccessResponse.of(HttpStatus.OK.value(), "์—ฌํ–‰ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ ์„ฑ๊ณต", travelListResponse); + } + +// ๊ณ„์ขŒ ์—ฐ๊ฒฐ + @PostMapping("/accounts") + public SuccessResponse postConnectedAccount(@RequestBody CardCompletedRequest request){ + Long userId = UserAuthorizationUtil.getLoginUserId(); + travelService.postConnectedAccount(request,userId); + return SuccessResponse.of(HttpStatus.OK.value(), "๊ณ„์ขŒ ์—ฐ๊ฒฐ ์™„๋ฃŒ"); + } + +// ์นด๋“œ ๋ฐœ๊ธ‰ ์—ฌ๋ถ€ + @GetMapping("/accounts") + public SuccessResponse getConnectedAccount(){ + Long userId = UserAuthorizationUtil.getLoginUserId(); + AccountConnectedWibeeResponse accountConnectedWibeeResponse = travelService.getConnectedWibee(userId); + return SuccessResponse.of(HttpStatus.OK.value(), "์œ„๋น„ ์นด๋“œ ๋ฐœ๊ธ‰ ์—ฌ๋ถ€ ํ™•์ธ",accountConnectedWibeeResponse); + } + + @Override + @CheckTravelAccess + @PatchMapping(value = "/{travelId}/main-image", consumes = "multipart/form-data") + public SuccessResponse changeMainImage( + @PathVariable Long travelId, + @RequestPart(value = "image") MultipartFile image + ) { + travelService.changeMainImage(travelId, image); + return SuccessResponse.of(HttpStatus.OK.value(), "์—ฌํ–‰ ๋ฉ”์ธ ์ด๋ฏธ์ง€๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/src/main/java/withbeetravel/controller/travel/TravelMemberController.java b/src/main/java/withbeetravel/controller/travel/TravelMemberController.java new file mode 100644 index 00000000..963010b8 --- /dev/null +++ b/src/main/java/withbeetravel/controller/travel/TravelMemberController.java @@ -0,0 +1,31 @@ +package withbeetravel.controller.travel; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import withbeetravel.aspect.CheckTravelAccess; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.travel.TravelMemberResponse; +import withbeetravel.service.travel.TravelMemberService; + +import java.util.List; + +@RestController +@RequestMapping("/api/travels/{travelId}/members") +@RequiredArgsConstructor +public class TravelMemberController { + + private final TravelMemberService travelMemberService; + + @CheckTravelAccess + @GetMapping + public SuccessResponse> getTravelMembers(@PathVariable Long travelId) { + return SuccessResponse.of(200, "์—ฌํ–‰ ๋ฉค๋ฒ„ ์กฐํšŒ ์„ฑ๊ณต", + travelMemberService.getTravelMembers(travelId) + .stream() + .map(TravelMemberResponse::from) + .toList()); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/controller/travel/docs/HoneyCapsuleControllerDocs.java b/src/main/java/withbeetravel/controller/travel/docs/HoneyCapsuleControllerDocs.java new file mode 100644 index 00000000..525c47d1 --- /dev/null +++ b/src/main/java/withbeetravel/controller/travel/docs/HoneyCapsuleControllerDocs.java @@ -0,0 +1,42 @@ +package withbeetravel.controller.travel.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.dto.response.travel.HoneyCapsuleResponse; +import withbeetravel.dto.response.SuccessResponse; + +import java.util.List; + +@Tag(name = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface HoneyCapsuleControllerDocs { + + @Operation( + summary = "ํ—ˆ๋‹ˆ์บก์А ์ •๋ณด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ API", + description = "ํ—ˆ๋‹ˆ์บก์А์— ๋Œ€ํ•œ ์ •๋ณด ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "์—ฌํ–‰"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "์—ฌํ–‰ ๊ธฐ๋ก ์กฐํšŒ ์„ฑ๊ณต", content = @Content(schema = @Schema(implementation = HoneyCapsuleResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "409", description = "SETTLEMENT-005", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse> getHoneyCapsuleList(Long travelId); +} diff --git a/src/main/java/withbeetravel/controller/travel/docs/TravelControllerDocs.java b/src/main/java/withbeetravel/controller/travel/docs/TravelControllerDocs.java new file mode 100644 index 00000000..e0d46959 --- /dev/null +++ b/src/main/java/withbeetravel/controller/travel/docs/TravelControllerDocs.java @@ -0,0 +1,51 @@ +package withbeetravel.controller.travel.docs; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.travel.HoneyCapsuleResponse; + +@Tag(name = "์—ฌํ–‰ API", description = "์— ๋Œ€ํ•œ ์„ค๋ช…์ž…๋‹ˆ๋‹ค.") +public interface TravelControllerDocs { + + @Operation( + summary = "์—ฌํ–‰ ๋ฉ”์ธ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ API", + description = "์—ฌํ–‰ ๋ฉ”์ธ ์ด๋ฏธ์ง€๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + tags = {"User Management", "์—ฌํ–‰"}, + parameters = { + @Parameter( + name = "travelId", + description = "์—ฌํ–‰ ID", + required = true, + in = ParameterIn.PATH, + example = "1234" + ), + @Parameter( + name = "file", + description = "์—ฌํ–‰ ๋ฉ”์ธ ์ด๋ฏธ์ง€๋กœ ๋ณ€๊ฒฝํ•  ํŒŒ์ผ", + required = true, + in = ParameterIn.DEFAULT, + example = "image.jpg" + ) + } + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "์—ฌํ–‰ ๊ธฐ๋ก ์กฐํšŒ ์„ฑ๊ณต", content = @Content(schema = @Schema(implementation = HoneyCapsuleResponse.class))), + @ApiResponse(responseCode = "401", description = "AUTH-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "TRAVEL-002", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "TRAVEL-001", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "422", description = "VALIDATION-004", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + }) + SuccessResponse changeMainImage( + Long travelId, + MultipartFile file + ); +} diff --git a/src/main/java/withbeetravel/domain/Account.java b/src/main/java/withbeetravel/domain/Account.java new file mode 100644 index 00000000..817c2ebf --- /dev/null +++ b/src/main/java/withbeetravel/domain/Account.java @@ -0,0 +1,56 @@ +package withbeetravel.domain; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "accounts") +@Getter +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "account_id", nullable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @JsonBackReference + private User user; + + @Column(name = "account_number", nullable = false, unique = true) + private String accountNumber; + + @Column(name = "balance",nullable = false) + private long balance; + + @Enumerated(EnumType.STRING) + @Column(name = "product", nullable = false) + private Product product; + + @Column(name = "is_connected_wibee_card", nullable = false) + private boolean isConnectedWibeeCard; + + public void transfer(int amount){ + balance += amount; + } + + protected Account(){}; + + @Builder + public Account(Long id, User user, + String accountNumber, long balance, + Product product, boolean isConnectedWibeeCard) { + this.id = id; + this.user = user; + this.accountNumber = accountNumber; + this.balance = balance; + this.product = product; + this.isConnectedWibeeCard = isConnectedWibeeCard; + } + + public void updatedAccount(Boolean isConnectedWibeeCard){ + this.isConnectedWibeeCard = isConnectedWibeeCard; + } +} diff --git a/src/main/java/withbeetravel/domain/Category.java b/src/main/java/withbeetravel/domain/Category.java index e61bda06..2e76e169 100644 --- a/src/main/java/withbeetravel/domain/Category.java +++ b/src/main/java/withbeetravel/domain/Category.java @@ -1,12 +1,33 @@ package withbeetravel.domain; +import lombok.Getter; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.PaymentErrorCode; +import withbeetravel.exception.error.TravelErrorCode; + +import java.util.Arrays; + +@Getter public enum Category { - TRANSPORTATION, // ๊ตํ†ต - FOOD, // ์‹๋น„ - ACCOMMODATION, // ์ˆ™๋ฐ• - TOUR, // ๊ด€๊ด‘ - ACTIVITY, // ์•กํ‹ฐ๋น„ํ‹ฐ - SHOPPING, // ์‡ผํ•‘ - FLIGHT, // ํ•ญ๊ณต - ETC // ๊ธฐํƒ€ + TRANSPORTATION("๊ตํ†ต"), + FOOD("์‹๋น„"), + ACCOMMODATION("์ˆ™๋ฐ•"), + TOUR("๊ด€๊ด‘"), + ACTIVITY("์•กํ‹ฐ๋น„ํ‹ฐ"), + SHOPPING("์‡ผํ•‘"), + FLIGHT("ํ•ญ๊ณต"), + ETC("๊ธฐํƒ€"); + + private final String description; + + Category(String description) { + this.description = description; + } + + public static Category fromString(String description) { + return Arrays.stream(values()) + .filter(category -> category.description.equals(description)) + .findFirst() + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_CATEGORY_NOT_FOUND)); + } } diff --git a/src/main/java/withbeetravel/domain/Country.java b/src/main/java/withbeetravel/domain/Country.java index b2bc95a2..96cbfcd0 100644 --- a/src/main/java/withbeetravel/domain/Country.java +++ b/src/main/java/withbeetravel/domain/Country.java @@ -1,5 +1,10 @@ package withbeetravel.domain; +import lombok.Getter; + +import java.util.List; + +@Getter public enum Country { US("๋ฏธ๊ตญ", "USD"), ES("์ŠคํŽ˜์ธ", "EUR"), @@ -59,4 +64,14 @@ public enum Country { this.countryName = countryName; this.currencyCode = currencyCode; } + + public static Country findByName(String countryName){ + String name = countryName.trim(); + + return List.of(Country.values()).stream() + .filter(country -> country.getCountryName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No enum constant for country name: " + countryName)); + } + } diff --git a/src/main/java/withbeetravel/domain/CurrencyUnit.java b/src/main/java/withbeetravel/domain/CurrencyUnit.java index 5886c351..122c6c1b 100644 --- a/src/main/java/withbeetravel/domain/CurrencyUnit.java +++ b/src/main/java/withbeetravel/domain/CurrencyUnit.java @@ -1,6 +1,20 @@ package withbeetravel.domain; + +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.ValidationErrorCode; + public enum CurrencyUnit { USD, EUR, JPY, CNY, KRW, GBP, AUD, CAD, NZD, MXN, INR, BRL, ZAR, SEK, NOK, DKK, CHF, HKD, SGD, THB, TRY, MYR, PHP, AED, SAR, KWD, BHD, QAR, OMR, JOD, LBP, EGP, IDR, PKR, TWD, VND, COP, PEN, CLP, ARS; + + public static CurrencyUnit from(String unit) { + + for (CurrencyUnit currencyUnit : CurrencyUnit.values()) { + if(currencyUnit.name().equalsIgnoreCase(unit)) { // ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„์—†์ด ๋น„๊ต + return currencyUnit; + } + } + throw new CustomException(ValidationErrorCode.INVALID_CURRENCY_UNIT); + } } diff --git a/src/main/java/withbeetravel/domain/History.java b/src/main/java/withbeetravel/domain/History.java new file mode 100644 index 00000000..7b55006e --- /dev/null +++ b/src/main/java/withbeetravel/domain/History.java @@ -0,0 +1,64 @@ +package withbeetravel.domain; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "histories") +public class History { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "history_id", nullable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @Column(name = "date",nullable = false) + private LocalDateTime date; + + @Column(name = "rcv_am") + private Integer rcvAm; + + @Column(name = "pay_am") + private Integer payAM; + + @Column(name = "balance", nullable = false) + private long balance; + + @Column(name = "rqspe_nm", nullable = false) + private String rqspeNm; + + @Column(name = "is_wibee_card", nullable = false) + private boolean isWibeeCard; + + @Column(name = "is_added_shared_payment", nullable = false) + private boolean isAddedSharedPayment; + + protected History(){}; + + @Builder + public History(Long id, Account account, LocalDateTime date, + Integer rcvAm, Integer payAM, long balance, + String rqspeNm, boolean isWibeeCard, + boolean isAddedSharedPayment) { + this.id = id; + this.account = account; + this.date = date; + this.rcvAm = rcvAm; + this.payAM = payAM; + this.balance = balance; + this.rqspeNm = rqspeNm; + this.isWibeeCard = isWibeeCard; + this.isAddedSharedPayment = isAddedSharedPayment; + } + + public void addedSharedPayment() { + this.isAddedSharedPayment = true; + } +} diff --git a/src/main/java/withbeetravel/domain/LogTitle.java b/src/main/java/withbeetravel/domain/LogTitle.java new file mode 100644 index 00000000..59539d62 --- /dev/null +++ b/src/main/java/withbeetravel/domain/LogTitle.java @@ -0,0 +1,68 @@ +package withbeetravel.domain; + +import lombok.Getter; + +import java.text.DecimalFormat; + +public enum LogTitle { + PAYMENT_REQUEST("๊ฒฐ์ œ ๋‚ด์—ญ ์ •๋ฆฌ ์š”์ฒญ", + "{0}์˜ ์—ฌํ–‰์ด ๋๋‚ฌ์–ด์š”! ๐Ÿš—๐Ÿ’จ
ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ ๋น„์šฉ๋“ค์„ ์ •๋ฆฌํ•ด ๋ณผ๊นŒ์š”? ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์„ ํ™•์ธํ•˜๊ณ  ๋งˆ๋ฌด๋ฆฌํ•ด ์ฃผ์„ธ์š”.", + "travel/{0}/payments"), + SETTLEMENT_REQUEST("์ •์‚ฐ ์š”์ฒญ", + "{0}์—์„œ ์ •์‚ฐ ์š”์ฒญ์„ ๋ณด๋ƒˆ์–ด์š”! ๐Ÿ’ธ
ํ•จ๊ป˜ํ•œ ๋น„์šฉ์„ ํ™•์ธํ•˜๊ณ , ๋‚˜์˜ ๋ชซ์„ ์ •์‚ฐํ•ด ์ฃผ์„ธ์š”.", + "travel/{0}/settlement"), + SETTLEMENT_PENDING("์ •์‚ฐ ๋ณด๋ฅ˜", + "{0}์˜ ์ •์‚ฐ์ด ๋ณด๋ฅ˜๋์–ด์š”! ๐Ÿ’ธ
" + + "{1}๋‹˜์˜ ์ž”์•ก์ด ๋ถ€์กฑํ•œ ์ƒํƒœ์—์š”. ๊ณ„์ขŒ์— ๋ˆ์„ ์ž…๊ธˆํ•˜๊ณ , ๋‹ค์‹œ ํ•œ ๋ฒˆ ์ •์‚ฐ์— ๋™์˜ํ•ด ์ฃผ์„ธ์š”. ๐Ÿ˜Š", + "banking/{0}"), + SETTLEMENT_CANCEL("์ •์‚ฐ ์ทจ์†Œ", + "{0}์˜ ์ •์‚ฐ ์š”์ฒญ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ˜Œ", + null), + SETTLEMENT_RE_REQUEST("์ •์‚ฐ ์žฌ์š”์ฒญ", + "{0}์˜ ์ •์‚ฐ ์š”์ฒญ์— ์•„์ง ๋™์˜ํ•˜์ง€ ์•Š์•˜์–ด์š”! ๐Ÿ˜…
ํ˜น์‹œ ์žŠ์œผ์‹  ๊ฑด ์•„๋‹Œ๊ฐ€์š”? ๋น ๋ฅด๊ฒŒ ์ •์‚ฐ์„ ์™„๋ฃŒํ•ด ์ฃผ์„ธ์š”.", + "travel/{0}/settlement"), + TRAVEL_MEMBER_ADDED("์—ฌํ–‰ ๋ฉค๋ฒ„ ํ•ฉ๋ฅ˜", + "{0}๋‹˜์ด {1}์— ํ•ฉ๋ฅ˜ํ–ˆ์–ด์š”! ๐ŸŽ‰
ํ•จ๊ป˜ ๋ฉ‹์ง„ ์ถ”์–ต์„ ๋งŒ๋“ค์–ด ๋ณด์„ธ์š”. ๐Ÿฅฐ", + "travel/{0}"), + SETTLEMENT_COMPLETE("์ •์‚ฐ ์™„๋ฃŒ", + "{0}์˜ ์ •์‚ฐ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ •์‚ฐ๊ธˆ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”! ๐ŸŽ‰
" + + "์œ„๋น„๊ฐ€ {1}์›์„ ์ง€์›ํ–ˆ์Šต๋‹ˆ๋‹ค.๐Ÿ€ ์œ„๋น„์™€ ํ•จ๊ป˜ํ•˜๋Š” ๋‹ค์Œ ์—ฌํ–‰๋„ ๊ธฐ๋Œ€ํ•ด์š”!", + "banking/{0}"); + + @Getter + private final String title; + private final String messageTemplate; + private final String linkPattern; + + LogTitle(String title, String messageTemplate, String linkPattern) { + this.title = title; + this.messageTemplate = messageTemplate; + this.linkPattern = linkPattern; + } + + public String getMessage(String travelName) { + return messageTemplate.replace("{0}", travelName); + } + + public String getMessage(String travelName, int additionalValue) { + if (additionalValue == 0) { + return travelName + "์˜ ์ •์‚ฐ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰
์œ„๋น„์™€ ํ•จ๊ป˜ํ•˜๋Š” ๋‹ค์Œ ์—ฌํ–‰๋„ ๊ธฐ๋Œ€ํ•ด์š”!"; + } + return messageTemplate + .replace("{0}", travelName) + .replace("{1}", formatter.format(additionalValue)); + } + + public String getMessage(String travelName, String name) { + return messageTemplate + .replace("{0}", travelName) + .replace("{1}", name); + } + + public String getLinkPattern(long num) { + return linkPattern + .replace("{0}", String.valueOf(num)); + } + + DecimalFormat formatter = new DecimalFormat("###,###"); +} diff --git a/src/main/java/withbeetravel/domain/LoginLog.java b/src/main/java/withbeetravel/domain/LoginLog.java new file mode 100644 index 00000000..14c77b8c --- /dev/null +++ b/src/main/java/withbeetravel/domain/LoginLog.java @@ -0,0 +1,50 @@ +package withbeetravel.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import java.time.LocalDateTime; + +@Entity +@Getter +@Table(name = "login_logs") +public class LoginLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "log_id", nullable = false) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "login_log_type", nullable = false) + private LoginLogType loginLogType; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name ="user_id",nullable = false) + private User user; + + @Column(name = "description") + private String description; // ์ถ”๊ฐ€์ ์ธ ์„ค๋ช… (์˜ˆ: ์‹คํŒจ ์ด์œ , ํŠธ๋žœ์žญ์…˜ ID ๋“ฑ) + + @CreationTimestamp + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; // ๋กœ๊ทธ ๋ฐœ์ƒ ์‹œ์  + + @Column(name = "ip_address") + private String ipAddress; + + protected LoginLog(){}; + + @Builder + public LoginLog(Long id, LoginLogType loginLogType, User user, + String description, LocalDateTime createdAt, String ipAddress) { + this.id = id; + this.loginLogType = loginLogType; + this.user = user; + this.description = description; + this.createdAt = createdAt; + this.ipAddress = ipAddress; + } +} diff --git a/src/main/java/withbeetravel/domain/LoginLogType.java b/src/main/java/withbeetravel/domain/LoginLogType.java new file mode 100644 index 00000000..cdd25ae4 --- /dev/null +++ b/src/main/java/withbeetravel/domain/LoginLogType.java @@ -0,0 +1,9 @@ +package withbeetravel.domain; + +public enum LoginLogType { + REGISTER,//๊ฐ€์ž… ์ผ์ž + LOGIN, // ๋กœ๊ทธ์ธ ๊ธฐ๋ก + LOGIN_FAILED, // ๋กœ๊ทธ์ธ ์‹คํŒจ ๊ธฐ๋ก + LOGOUT // ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋ก + +} diff --git a/src/main/java/withbeetravel/domain/PaymentParticipatedMember.java b/src/main/java/withbeetravel/domain/PaymentParticipatedMember.java index 19d51cda..a7df2829 100644 --- a/src/main/java/withbeetravel/domain/PaymentParticipatedMember.java +++ b/src/main/java/withbeetravel/domain/PaymentParticipatedMember.java @@ -1,12 +1,15 @@ package withbeetravel.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @Table(name = "payment_participated_members") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class PaymentParticipatedMember { @Id @@ -22,12 +25,14 @@ public class PaymentParticipatedMember { @JoinColumn(name = "shared_payment_id", nullable = false) private SharedPayment sharedPayment; - protected PaymentParticipatedMember() {} - @Builder public PaymentParticipatedMember(Long id, TravelMember travelMember, SharedPayment sharedPayment) { this.id = id; this.travelMember = travelMember; this.sharedPayment = sharedPayment; } + + public void assignSharedPayment(SharedPayment sharedPayment) { + this.sharedPayment = sharedPayment; + } } diff --git a/src/main/java/withbeetravel/domain/Product.java b/src/main/java/withbeetravel/domain/Product.java new file mode 100644 index 00000000..06483024 --- /dev/null +++ b/src/main/java/withbeetravel/domain/Product.java @@ -0,0 +1,7 @@ +package withbeetravel.domain; + +public enum Product { + WONํ†ต์žฅ, ์šฐ๋ฆฌ๋‹ท์ปดํ†ต์žฅ, ์šฐ๋ฆฌ์•„์ดํ–‰๋ณตํ†ต์žฅ, WONํŒŒํ‚นํ†ต์žฅ,์œผ์“ฑํ†ต์žฅ, ๋ณดํ†ต์˜ˆ๊ธˆ + +} + diff --git a/src/main/java/withbeetravel/domain/RefreshToken.java b/src/main/java/withbeetravel/domain/RefreshToken.java new file mode 100644 index 00000000..62fd9a13 --- /dev/null +++ b/src/main/java/withbeetravel/domain/RefreshToken.java @@ -0,0 +1,37 @@ +package withbeetravel.domain; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Entity +@Table(name = "refresh_tokens") +@Getter +@NoArgsConstructor +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "token") + private String token; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "expiration_time", nullable = false) + private Date expirationTime; + + @Builder + public RefreshToken(Long id, User user, String token, Date expirationTime) { + this.id = id; + this.user = user; + this.token = token; + this.expirationTime = expirationTime; + } +} diff --git a/src/main/java/withbeetravel/domain/RequestStatus.java b/src/main/java/withbeetravel/domain/RequestStatus.java deleted file mode 100644 index ee9e8400..00000000 --- a/src/main/java/withbeetravel/domain/RequestStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package withbeetravel.domain; - -public enum RequestStatus { - ONGOING, - DONE, - CANCELED; -} diff --git a/src/main/java/withbeetravel/domain/RoleType.java b/src/main/java/withbeetravel/domain/RoleType.java new file mode 100644 index 00000000..025a32b8 --- /dev/null +++ b/src/main/java/withbeetravel/domain/RoleType.java @@ -0,0 +1,5 @@ +package withbeetravel.domain; + +public enum RoleType { + USER, ADMIN +} diff --git a/src/main/java/withbeetravel/domain/SettlementRequest.java b/src/main/java/withbeetravel/domain/SettlementRequest.java index 835879d1..dae73316 100644 --- a/src/main/java/withbeetravel/domain/SettlementRequest.java +++ b/src/main/java/withbeetravel/domain/SettlementRequest.java @@ -1,8 +1,10 @@ package withbeetravel.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; @@ -10,6 +12,7 @@ @Entity @Getter @Table(name = "settlement_requests") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SettlementRequest { @Id @@ -17,8 +20,8 @@ public class SettlementRequest { @Column(name = "settlement_request_id", nullable = false) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "travel_id", nullable = false) + @OneToOne + @JoinColumn(name = "travel_id") private Travel travel; @CreationTimestamp @@ -28,23 +31,27 @@ public class SettlementRequest { @Column(name = "request_end_time") private LocalDateTime requestEndTime; - - @Column(name = "request_status", nullable = false) - @Enumerated(EnumType.STRING) - private RequestStatus requestStatus; - - protected SettlementRequest() {} + @Column(name = "disagree_count", nullable = false) + private int disagreeCount; @Builder public SettlementRequest(Long id, Travel travel, LocalDateTime requestStartTime, LocalDateTime requestEndTime, - RequestStatus requestStatus) { + int disagreeCount) { this.id = id; this.travel = travel; this.requestStartTime = requestStartTime; this.requestEndTime = requestEndTime; - this.requestStatus = requestStatus; + this.disagreeCount = disagreeCount; + } + + public void updateDisagreeCount(int count) { + this.disagreeCount += count; + } + + public void updateRequestEndDate(LocalDateTime endTime) { + this.requestEndTime = endTime; } } diff --git a/src/main/java/withbeetravel/domain/SettlementRequestLog.java b/src/main/java/withbeetravel/domain/SettlementRequestLog.java index 3eea3f85..9243d592 100644 --- a/src/main/java/withbeetravel/domain/SettlementRequestLog.java +++ b/src/main/java/withbeetravel/domain/SettlementRequestLog.java @@ -1,14 +1,16 @@ package withbeetravel.domain; import jakarta.persistence.*; -import lombok.Builder; -import lombok.Getter; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; @Entity @Getter @Table(name = "settlement_request_logs") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString public class SettlementRequestLog { @Id @@ -17,22 +19,41 @@ public class SettlementRequestLog { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "settlement_request_id", nullable = false) - private SettlementRequest settlementRequest; + @JoinColumn(name = "travel_id", nullable = false) + private Travel travel; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "log_title", nullable = false) + @Enumerated(EnumType.STRING) + private LogTitle logTitle; @Column(name = "log_message", nullable = false) private String logMessage; - @Column(name = "log_time", nullable = false) + @CreationTimestamp + @Column(name = "log_time") private LocalDateTime logTime; - protected SettlementRequestLog() {} + @Column(name = "link") + private String link; @Builder - public SettlementRequestLog(Long id, SettlementRequest settlementRequest, String logMessage, LocalDateTime logTime) { + public SettlementRequestLog(Long id, + Travel travel, + User user, + LogTitle logTitle, + String logMessage, + LocalDateTime logTime, + String link) { this.id = id; - this.settlementRequest = settlementRequest; + this.travel = travel; + this.user = user; + this.logTitle = logTitle; this.logMessage = logMessage; this.logTime = logTime; + this.link = link; } } diff --git a/src/main/java/withbeetravel/domain/SharedPayment.java b/src/main/java/withbeetravel/domain/SharedPayment.java index d0f9fc5d..ec101595 100644 --- a/src/main/java/withbeetravel/domain/SharedPayment.java +++ b/src/main/java/withbeetravel/domain/SharedPayment.java @@ -1,14 +1,19 @@ package withbeetravel.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @Table(name = "shared_payments") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class SharedPayment { @Id @@ -20,6 +25,10 @@ public class SharedPayment { @JoinColumn(name = "added_by_member_id", nullable = false) private TravelMember addedByMember; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "travel_id", nullable = false) + private Travel travel; + @Column(name = "currency_unit", nullable = false) @Enumerated(EnumType.STRING) private CurrencyUnit currencyUnit; @@ -28,22 +37,22 @@ public class SharedPayment { private int paymentAmount; @Column(name = "foreign_payment_amount") - private double foreignPaymentAmount; + private Double foreignPaymentAmount; @Column(name = "exchange_rate") - private double exchangeRate; + private Double exchangeRate; @Column(name = "payment_comment") private String paymentComment; @Column(name = "payment_image") - private String profileImage; + private String paymentImage; @Column(name = "is_manually_added", nullable = false) - private int isManuallyAdded; + private boolean isManuallyAdded; - @Column(name = "is_all_members_participated", nullable = false) - private int isAllMembersParticipated; + @Column(name = "participant_count", nullable = false) + private int participantCount; @Column(name = "category", nullable = false) @Enumerated(EnumType.STRING) @@ -55,34 +64,76 @@ public class SharedPayment { @Column(name = "payment_date", nullable = false) private LocalDateTime paymentDate; - protected SharedPayment() {} + @OneToMany(mappedBy = "sharedPayment") + private List paymentParticipatedMembers = new ArrayList<>(); @Builder public SharedPayment(Long id, TravelMember addedByMember, + Travel travel, CurrencyUnit currencyUnit, int paymentAmount, - double foreignPaymentAmount, - double exchangeRate, + Double foreignPaymentAmount, + Double exchangeRate, String paymentComment, - String profileImage, - int isManuallyAdded, - int isAllMembersParticipated, + String paymentImage, + boolean isManuallyAdded, + int participantCount, Category category, String storeName, LocalDateTime paymentDate) { this.id = id; this.addedByMember = addedByMember; + this.travel = travel; this.currencyUnit = currencyUnit; this.paymentAmount = paymentAmount; this.foreignPaymentAmount = foreignPaymentAmount; this.exchangeRate = exchangeRate; this.paymentComment = paymentComment; - this.profileImage = profileImage; + this.paymentImage = paymentImage; this.isManuallyAdded = isManuallyAdded; - this.isAllMembersParticipated = isAllMembersParticipated; + this.participantCount = participantCount; + this.category = category; + this.storeName = storeName; + this.paymentDate = paymentDate; + } + + public void updatePaymentImage(String newPaymentImage) { + this.paymentImage = newPaymentImage; + } + + public void updatePaymentCommnet(String newPaymentComment) { + this.paymentComment = newPaymentComment; + } + + public void updateParticipantCount(int newParticipantCount) { + this.participantCount = newParticipantCount; + } + + public void updateManuallyPayment( + CurrencyUnit currencyUnit, + int paymentAmount, + Double foreignPaymentAmount, + Double exchangeRate, + String paymentComment, + String paymentImage, + Category category, + String storeName, + LocalDateTime paymentDate + ) { + this.currencyUnit = currencyUnit; + this.paymentAmount = paymentAmount; + this.foreignPaymentAmount = foreignPaymentAmount; + this.exchangeRate = exchangeRate; + this.paymentComment = paymentComment; + this.paymentImage = paymentImage; this.category = category; this.storeName = storeName; this.paymentDate = paymentDate; } + + public void addPaymentParticipatedMember(PaymentParticipatedMember member) { + this.paymentParticipatedMembers.add(member); + member.assignSharedPayment(this); + } } diff --git a/src/main/java/withbeetravel/domain/Travel.java b/src/main/java/withbeetravel/domain/Travel.java index 93885c9a..6b430b80 100644 --- a/src/main/java/withbeetravel/domain/Travel.java +++ b/src/main/java/withbeetravel/domain/Travel.java @@ -1,13 +1,19 @@ package withbeetravel.domain; import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @Table(name = "travels") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Travel { @Id @@ -31,20 +37,27 @@ public class Travel { private String mainImage; @Column(name = "is_domestic_travel", nullable = false) - private int isDomesticTravel; + private boolean isDomesticTravel; @Column(name = "settlement_status", nullable = false) @Enumerated(value = EnumType.STRING) private SettlementStatus settlementStatus; - protected Travel() {} + @OneToMany(mappedBy = "travel") + private List travelMembers = new ArrayList<>(); + @OneToMany(mappedBy = "travel") + private List countries = new ArrayList<>(); + + @Builder public Travel(Long id, String travelName, LocalDate travelStartDate, LocalDate travelEndDate, String inviteCode, - String mainImage, int isDomesticTravel, SettlementStatus settlementStatus) { + String mainImage, + boolean isDomesticTravel, + SettlementStatus settlementStatus) { this.id = id; this.travelName = travelName; this.travelStartDate = travelStartDate; @@ -54,4 +67,21 @@ public Travel(Long id, this.isDomesticTravel = isDomesticTravel; this.settlementStatus = settlementStatus; } + + public void updateTravel(String travelName, LocalDate travelStartDate, LocalDate travelEndDate, boolean isDomesticTravel) { + this.travelName = travelName; + this.travelStartDate = travelStartDate; + this.travelEndDate = travelEndDate; + this.isDomesticTravel = isDomesticTravel; + } + + + + public void updateMainImage(String newMainImage) { + this.mainImage = newMainImage; + } + + public void updateSettlementStatus(SettlementStatus newSettlementStatus) { + this.settlementStatus = newSettlementStatus; + } } diff --git a/src/main/java/withbeetravel/domain/TravelCountry.java b/src/main/java/withbeetravel/domain/TravelCountry.java index 222c1f4e..c911ca83 100644 --- a/src/main/java/withbeetravel/domain/TravelCountry.java +++ b/src/main/java/withbeetravel/domain/TravelCountry.java @@ -1,12 +1,15 @@ package withbeetravel.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @Table(name = "travel_countries") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class TravelCountry { @Id @@ -18,12 +21,10 @@ public class TravelCountry { @Enumerated(value = EnumType.STRING) private Country country; - @JoinColumn(name = "travel_id", nullable = false) @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "travel_id", nullable = false) private Travel travel; - protected TravelCountry() {} - @Builder public TravelCountry(Long id, Country country, Travel travel) { this.id = id; diff --git a/src/main/java/withbeetravel/domain/TravelMember.java b/src/main/java/withbeetravel/domain/TravelMember.java index 43077c2c..6b612492 100644 --- a/src/main/java/withbeetravel/domain/TravelMember.java +++ b/src/main/java/withbeetravel/domain/TravelMember.java @@ -23,19 +23,22 @@ public class TravelMember { private User user; @Column(name = "is_captain", nullable = false) - private int isCaptain; + private boolean isCaptain; - @Column(name = "connected_account", nullable = false) - private String connectedAccount; + @OneToOne(mappedBy = "travelMember") + private TravelMemberSettlementHistory settlementHistory; protected TravelMember() {} @Builder - public TravelMember(Long id, Travel travel, User user, int isCaptain, String connectedAccount) { + public TravelMember(Long id, Travel travel, User user, boolean isCaptain) { this.id = id; this.travel = travel; this.user = user; this.isCaptain = isCaptain; - this.connectedAccount = connectedAccount; + } + + public void initializeSettlementHistory() { + settlementHistory = null; } } diff --git a/src/main/java/withbeetravel/domain/TravelMemberSettlementHistory.java b/src/main/java/withbeetravel/domain/TravelMemberSettlementHistory.java index 127e6249..3c16a004 100644 --- a/src/main/java/withbeetravel/domain/TravelMemberSettlementHistory.java +++ b/src/main/java/withbeetravel/domain/TravelMemberSettlementHistory.java @@ -1,12 +1,15 @@ package withbeetravel.domain; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter @Table(name = "travel_member_settlement_histories") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class TravelMemberSettlementHistory { @Id @@ -23,23 +26,21 @@ public class TravelMemberSettlementHistory { private TravelMember travelMember; @Column(name = "own_payment_cost", nullable = false) - private double ownPaymentCost; + private int ownPaymentCost; @Column(name = "actual_burden_cost", nullable = false) - private double actualBurdenCost; + private int actualBurdenCost; @Column(name = "is_agreed", nullable = false) - private int isAgreed; - - protected TravelMemberSettlementHistory() {} + private boolean isAgreed; @Builder public TravelMemberSettlementHistory(Long id, SettlementRequest settlementRequest, TravelMember travelMember, - double ownPaymentCost, - double actualBurdenCost, - int isAgreed) { + int ownPaymentCost, + int actualBurdenCost, + boolean isAgreed) { this.id = id; this.settlementRequest = settlementRequest; this.travelMember = travelMember; @@ -47,4 +48,8 @@ public TravelMemberSettlementHistory(Long id, this.actualBurdenCost = actualBurdenCost; this.isAgreed = isAgreed; } + + public void updateIsAgreed(boolean isAgreed) { + this.isAgreed = isAgreed; + } } diff --git a/src/main/java/withbeetravel/domain/User.java b/src/main/java/withbeetravel/domain/User.java index 3c5018ac..e3c47e74 100644 --- a/src/main/java/withbeetravel/domain/User.java +++ b/src/main/java/withbeetravel/domain/User.java @@ -1,12 +1,17 @@ package withbeetravel.domain; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCrypt; @Entity @Getter @Table(name = "users") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class User { @Id @@ -14,6 +19,16 @@ public class User { @Column(name = "user_id", nullable = false) private Long id; + @OneToOne + @JoinColumn(name = "wibee_card_account") + @JsonManagedReference + private Account wibeeCardAccount; + + @OneToOne + @JoinColumn(name = "connected_account") + @JsonManagedReference + private Account connectedAccount; + @Column(name = "email", nullable = false) private String email; @@ -26,22 +41,60 @@ public class User { @Column(name = "name", nullable = false) private String name; - @Column(name = "has_card", nullable = false) - private int hasCard; - @Column(name = "profile_image", nullable = false) - private String profileImage; + private int profileImage; + + @Column(name = "failed_pin_count", nullable = false) + private int failedPinCount; + + @Column(name = "pin_locked", nullable = false) + private boolean pinLocked; + + @Column(name = "role", nullable = false) + @Enumerated(EnumType.STRING) + private RoleType roleType; - protected User() {} @Builder - public User(Long id, String email, String password, String pinNumber, String name, int hasCard, String profileImage) { + public User(Long id, Account wibeeCardAccount, Account connectedAccount, String email, + String password, String pinNumber, String name, + int profileImage, int failedPinCount, boolean pinLocked, RoleType roleType) { this.id = id; + this.wibeeCardAccount = wibeeCardAccount; + this.connectedAccount = connectedAccount; this.email = email; this.password = password; - this.pinNumber = pinNumber; + this.pinNumber = BCrypt.hashpw(pinNumber, BCrypt.gensalt()); this.name = name; - this.hasCard = hasCard; this.profileImage = profileImage; + this.failedPinCount = failedPinCount; + this.pinLocked = pinLocked; + this.roleType = roleType; + } + + public void updateWibeeCardAccount(Account account) { + this.wibeeCardAccount = account; } + + public void updateConnectedAccount(Account account){ + this.connectedAccount = account; + } + + public void incrementFailedPinCount() { + this.failedPinCount++; + if (this.failedPinCount >= 5) { + this.pinLocked = true; // ์‹คํŒจ 5ํšŒ ์ด์ƒ ์‹œ ๊ณ„์ • ์ž ๊ธˆ + } + } + + public void resetFailedPinCount() { + this.failedPinCount = 0; + this.pinLocked = false; // ๊ณ„์ • ์ž ๊ธˆ ํ•ด์ œ + } + + public boolean validatePin(String rawPin) { + // ์˜ˆ์‹œ: BCrypt๋กœ ๋น„๊ตํ•˜๋Š” ๋ฐฉ๋ฒ• + return BCrypt.checkpw(rawPin, this.pinNumber); + } + } diff --git a/src/main/java/withbeetravel/dto/request/account/AccountNumberRequest.java b/src/main/java/withbeetravel/dto/request/account/AccountNumberRequest.java new file mode 100644 index 00000000..f22ebc3e --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/AccountNumberRequest.java @@ -0,0 +1,8 @@ +package withbeetravel.dto.request.account; + +import lombok.Data; + +@Data +public class AccountNumberRequest { + private String accountNumber; +} diff --git a/src/main/java/withbeetravel/dto/request/account/AccountRequest.java b/src/main/java/withbeetravel/dto/request/account/AccountRequest.java new file mode 100644 index 00000000..d94c451d --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/AccountRequest.java @@ -0,0 +1,19 @@ +package withbeetravel.dto.request.account; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import withbeetravel.domain.Product; + +@Getter +@AllArgsConstructor +@Builder +public class AccountRequest { + + private String accountNumber; + private long balance; + private Product product; + private boolean isConnected; + + +} diff --git a/src/main/java/withbeetravel/dto/request/account/CardCompletedRequest.java b/src/main/java/withbeetravel/dto/request/account/CardCompletedRequest.java new file mode 100644 index 00000000..e811cc71 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/CardCompletedRequest.java @@ -0,0 +1,14 @@ +package withbeetravel.dto.request.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class CardCompletedRequest { + +// ์นด๋“œ ๋ฐœ๊ธ‰ ์—†์ด ๊ณ„์ขŒ ์—ฐ๊ฒฐ -> wibeeCardAccountId : null, connectedAccountId : ๊ณ„์ขŒ๋ฒˆํ˜ธ +// ์นด๋“œ ๋ฐœ๊ธ‰ ๊ณ„์ขŒ ์—ฐ๊ฒฐ -> wibeeCardAccountId : ๊ณ„์ขŒ ๋ฒˆํ˜ธ, connectedAccountId : ๊ณ„์ขŒ๋ฒˆํ˜ธ + private Long accountId; + private boolean isWibeeCard; +} diff --git a/src/main/java/withbeetravel/dto/request/account/CreateAccountRequest.java b/src/main/java/withbeetravel/dto/request/account/CreateAccountRequest.java new file mode 100644 index 00000000..351b34db --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/CreateAccountRequest.java @@ -0,0 +1,15 @@ +package withbeetravel.dto.request.account; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import withbeetravel.domain.Product; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateAccountRequest { + private Product product; +} diff --git a/src/main/java/withbeetravel/dto/request/account/DepositRequest.java b/src/main/java/withbeetravel/dto/request/account/DepositRequest.java new file mode 100644 index 00000000..ab7717cf --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/DepositRequest.java @@ -0,0 +1,9 @@ +package withbeetravel.dto.request.account; + +import lombok.Getter; + +@Getter +public class DepositRequest { + private int amount; + private String rqspeNm; +} diff --git a/src/main/java/withbeetravel/dto/request/account/HistoryRequest.java b/src/main/java/withbeetravel/dto/request/account/HistoryRequest.java new file mode 100644 index 00000000..97a9d792 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/HistoryRequest.java @@ -0,0 +1,29 @@ +package withbeetravel.dto.request.account; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +@Schema(description = "๊ณ„์ขŒ ๋‚ด์—ญ Request DTO") +public class HistoryRequest { + + @Schema( + description = "๊ฒฐ์ œ ๊ธˆ์•ก", + example = "8900" + ) + private Integer payAm; + + @Schema( + description = "์ƒํ˜ธ๋ช…", + example = "Tokyo Banana" + ) + private String rqspeNm; + + @Schema( + description = "์œ„๋น„์นด๋“œ๋กœ ๊ฒฐ์ œ ์—ฌ๋ถ€", + example = "true/false" + ) + @JsonProperty("isWibeeCard") + private boolean isWibeeCard; +} diff --git a/src/main/java/withbeetravel/dto/request/account/PinNumberRequest.java b/src/main/java/withbeetravel/dto/request/account/PinNumberRequest.java new file mode 100644 index 00000000..0ebb4bfc --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/PinNumberRequest.java @@ -0,0 +1,8 @@ +package withbeetravel.dto.request.account; + +import lombok.Getter; + +@Getter +public class PinNumberRequest { + private String pinNumber; +} diff --git a/src/main/java/withbeetravel/dto/request/account/TransferRequest.java b/src/main/java/withbeetravel/dto/request/account/TransferRequest.java new file mode 100644 index 00000000..3d444dc9 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/account/TransferRequest.java @@ -0,0 +1,12 @@ +package withbeetravel.dto.request.account; + +import lombok.Getter; + +@Getter +public class TransferRequest { + + private Long accountId; + private int amount; + private String accountNumber; + private String rqspeNm; +} diff --git a/src/main/java/withbeetravel/dto/request/admin/LoginLogRequest.java b/src/main/java/withbeetravel/dto/request/admin/LoginLogRequest.java new file mode 100644 index 00000000..08106ad9 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/admin/LoginLogRequest.java @@ -0,0 +1,26 @@ +package withbeetravel.dto.request.admin; + +import lombok.Getter; +import withbeetravel.domain.LoginLogType; + +@Getter +public class LoginLogRequest { + private Long userId; + private int page; + private int size; + private String loginLogType; + + public LoginLogRequest(Long userId, int page, int size, String loginLogType) { + this.userId = userId; + this.page = page; + this.size = size; + this.loginLogType = loginLogType; + } + + public LoginLogType getLoginLogType() { + if (loginLogType != null) { + return LoginLogType.valueOf(loginLogType.toUpperCase()); // String์„ Enum์œผ๋กœ ๋ณ€ํ™˜ + } + return null; + } +} diff --git a/src/main/java/withbeetravel/dto/request/admin/TravelAdminRequest.java b/src/main/java/withbeetravel/dto/request/admin/TravelAdminRequest.java new file mode 100644 index 00000000..590620c5 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/admin/TravelAdminRequest.java @@ -0,0 +1,12 @@ +package withbeetravel.dto.request.admin; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TravelAdminRequest { + private int page; + private int size; + private Long userId; +} diff --git a/src/main/java/withbeetravel/dto/request/admin/UserRequest.java b/src/main/java/withbeetravel/dto/request/admin/UserRequest.java new file mode 100644 index 00000000..368a7d27 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/admin/UserRequest.java @@ -0,0 +1,13 @@ +package withbeetravel.dto.request.admin; + +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +@Data +@Builder +public class UserRequest { + private String name; + private int page; + private int size; +} diff --git a/src/main/java/withbeetravel/dto/request/auth/CustomUserInfo.java b/src/main/java/withbeetravel/dto/request/auth/CustomUserInfo.java new file mode 100644 index 00000000..b9d74c26 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/auth/CustomUserInfo.java @@ -0,0 +1,39 @@ +package withbeetravel.dto.request.auth; + +import lombok.Builder; +import lombok.Getter; +import withbeetravel.domain.RoleType; +import withbeetravel.domain.User; + +// ๋กœ์ง ๋‚ด๋ถ€์—์„œ ์ธ์ฆ ์œ ์ € ์ •๋ณด๋ฅผ ์ €์žฅํ•ด๋‘˜ dto +@Getter +public class CustomUserInfo { + private Long id; + private String email; + private String password; + private String name; + private RoleType role; + + @Builder + public CustomUserInfo(Long id, + String email, + String password, + String name, + RoleType role) { + this.id = id; + this.email = email; + this.password = password; + this.name = name; + this.role = role; + } + + public static CustomUserInfo from(User user) { + return CustomUserInfo.builder() + .id(user.getId()) + .email(user.getEmail()) + .password(user.getPassword()) + .name(user.getName()) + .role(user.getRoleType()) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/request/auth/SignInRequest.java b/src/main/java/withbeetravel/dto/request/auth/SignInRequest.java new file mode 100644 index 00000000..d730b6c9 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/auth/SignInRequest.java @@ -0,0 +1,21 @@ +package withbeetravel.dto.request.auth; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class SignInRequest { + + @NotNull(message = "์ด๋ฉ”์ผ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + @Email(message = "์ž˜๋ชป๋œ ์ด๋ฉ”์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.") + private String email; + + @NotNull(message = "๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.") + private String password; + + public SignInRequest(String email, String password) { + this.email = email; + this.password = password; + } +} diff --git a/src/main/java/withbeetravel/dto/request/auth/SignUpRequest.java b/src/main/java/withbeetravel/dto/request/auth/SignUpRequest.java new file mode 100644 index 00000000..ae23fcea --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/auth/SignUpRequest.java @@ -0,0 +1,32 @@ +package withbeetravel.dto.request.auth; + +import jakarta.validation.constraints.*; +import lombok.Getter; + +// ํšŒ์›๊ฐ€์ž… DTO +@Getter +public class SignUpRequest { + + @NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") + @Email(message = "์ž˜๋ชป๋œ ์ด๋ฉ”์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.") + private String email; + + @NotBlank(message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") + @Pattern( + regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*]).{10,}$", + message = "๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์˜์–ด, ์ˆซ์ž, ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด ์ตœ์†Œ 10์ž ์ด์ƒ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + private String password; + + @NotBlank(message = "ํ•€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") + private String pinNumber; + + @NotBlank(message = "์ด๋ฆ„์€ ํ•„์ˆ˜ ์ž…๋ ฅ ๊ฐ’์ž…๋‹ˆ๋‹ค.") + private String name; + + public SignUpRequest(String email, String password, String pinNumber, String name) { + this.email = email; + this.password = password; + this.pinNumber = pinNumber; + this.name = name; + } +} diff --git a/src/main/java/withbeetravel/dto/request/payment/SharedPaymentParticipateRequest.java b/src/main/java/withbeetravel/dto/request/payment/SharedPaymentParticipateRequest.java new file mode 100644 index 00000000..9724b8b7 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/payment/SharedPaymentParticipateRequest.java @@ -0,0 +1,21 @@ +package withbeetravel.dto.request.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Schema(description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ฐธ์—ฌ ๋ฉค๋ฒ„ ์ˆ˜์ • Request DTO") +@NoArgsConstructor +@AllArgsConstructor +public class SharedPaymentParticipateRequest { + + @Schema( + description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ฐธ์—ฌ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ", + example = "[17, 19, 22, 27]" + ) + private List travelMembersId; +} diff --git a/src/main/java/withbeetravel/dto/request/payment/SharedPaymentSearchRequest.java b/src/main/java/withbeetravel/dto/request/payment/SharedPaymentSearchRequest.java new file mode 100644 index 00000000..bc4af691 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/payment/SharedPaymentSearchRequest.java @@ -0,0 +1,35 @@ +package withbeetravel.dto.request.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +@Data +@Schema(description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ํ•„ํ„ฐ Request DTO") +public class SharedPaymentSearchRequest { + @Schema(description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ") + private int page = 0; + + @Schema(description = "์ •๋ ฌ ๊ธฐ์ค€ (latest/amount)", defaultValue = "latest", allowableValues = {"latest", "amount"}) + @Pattern(regexp = "^(latest|amount)$", message = "์ •๋ ฌ ๊ธฐ์ค€์€ latest ๋˜๋Š” amount๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.") + private String sortBy = "latest"; + + @Schema(description = "์‹œ์ž‘ ๋‚ ์งœ") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @Schema(description = "์ข…๋ฃŒ ๋‚ ์งœ") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endDate; + + @Schema(description = "ํŠน์ • ๋ฉค๋ฒ„์˜ ๊ฒฐ์ œ๋งŒ ๋ณด๊ธฐ") + private Long memberId; + + @Schema(description = "์—ฌํ–‰ ์นดํ…Œ๊ณ ๋ฆฌ", allowableValues = {"ํ•ญ๊ณต", "๊ตํ†ต", "์ˆ™๋ฐ•", "์‹๋น„", "๊ด€๊ด‘", "์•กํ‹ฐ๋น„ํ‹ฐ", "์‡ผํ•‘", "๊ธฐํƒ€"}) + @Pattern(regexp = "^(ํ•ญ๊ณต|๊ตํ†ต|์ˆ™๋ฐ•|์‹๋น„|๊ด€๊ด‘|์•กํ‹ฐ๋น„ํ‹ฐ|์‡ผํ•‘|๊ธฐํƒ€)?$", + message = "์œ ํšจํ•˜์ง€ ์•Š์€ ์นดํ…Œ๊ณ ๋ฆฌ์ž…๋‹ˆ๋‹ค.") + private String category; +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/request/payment/SharedPaymentWibeeCardRegisterRequest.java b/src/main/java/withbeetravel/dto/request/payment/SharedPaymentWibeeCardRegisterRequest.java new file mode 100644 index 00000000..8b781981 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/payment/SharedPaymentWibeeCardRegisterRequest.java @@ -0,0 +1,17 @@ +package withbeetravel.dto.request.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์— ์ถ”๊ฐ€ Request DTO") +public class SharedPaymentWibeeCardRegisterRequest { + + @Schema( + description = "์œ„๋น„ ์นด๋“œ ์—ฐ๋™ ๊ณ„์ขŒ์˜ ๊ณ„์ขŒ ๋‚ด์—ญ Id", + example = "[17, 19, 22, 27]" + ) + List historyId; +} diff --git a/src/main/java/withbeetravel/dto/request/settlementRequestLog/SettlementRequestLogDto.java b/src/main/java/withbeetravel/dto/request/settlementRequestLog/SettlementRequestLogDto.java new file mode 100644 index 00000000..a0df8f35 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/settlementRequestLog/SettlementRequestLogDto.java @@ -0,0 +1,35 @@ +package withbeetravel.dto.request.settlementRequestLog; + +import lombok.Builder; +import lombok.Getter; +import withbeetravel.domain.SettlementRequestLog; + +import java.time.LocalDateTime; + +@Getter +public class SettlementRequestLogDto { + private Long id; + private LocalDateTime logTime; + private String logTitle; + private String logMessage; + private String link; + + @Builder + public SettlementRequestLogDto(Long id, LocalDateTime logTime, String logTitle, String logMessage, String link) { + this.id = id; + this.logTime = logTime; + this.logTitle = logTitle; + this.logMessage = logMessage; + this.link = link; + } + + public static SettlementRequestLogDto of (SettlementRequestLog settlementRequestLog, String link) { + return SettlementRequestLogDto.builder() + .id(settlementRequestLog.getId()) + .logTime(settlementRequestLog.getLogTime()) + .logTitle(settlementRequestLog.getLogTitle().getTitle()) + .logMessage(settlementRequestLog.getLogMessage()) + .link(link) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/request/travel/InviteCodeSignUpRequest.java b/src/main/java/withbeetravel/dto/request/travel/InviteCodeSignUpRequest.java new file mode 100644 index 00000000..d9d7e888 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/travel/InviteCodeSignUpRequest.java @@ -0,0 +1,17 @@ +package withbeetravel.dto.request.travel; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class InviteCodeSignUpRequest { + + private String inviteCode; + + public InviteCodeSignUpRequest(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/withbeetravel/dto/request/travel/TravelRequest.java b/src/main/java/withbeetravel/dto/request/travel/TravelRequest.java new file mode 100644 index 00000000..2c23d557 --- /dev/null +++ b/src/main/java/withbeetravel/dto/request/travel/TravelRequest.java @@ -0,0 +1,24 @@ +package withbeetravel.dto.request.travel; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +public class TravelRequest { + + private String travelName; + + private boolean isDomesticTravel; + + private List travelCountries; + + private String travelStartDate; + + private String travelEndDate; + +} diff --git a/src/main/java/withbeetravel/exception/dto/ErrorResponseDto.java b/src/main/java/withbeetravel/dto/response/ErrorResponse.java similarity index 54% rename from src/main/java/withbeetravel/exception/dto/ErrorResponseDto.java rename to src/main/java/withbeetravel/dto/response/ErrorResponse.java index 03110b11..abb4a813 100644 --- a/src/main/java/withbeetravel/exception/dto/ErrorResponseDto.java +++ b/src/main/java/withbeetravel/dto/response/ErrorResponse.java @@ -1,5 +1,6 @@ -package withbeetravel.exception.dto; +package withbeetravel.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; import org.springframework.http.ResponseEntity; @@ -7,18 +8,26 @@ @Getter @Builder -public class ErrorResponseDto { +@Schema(description = "Error์— ๋Œ€ํ•œ Response DTO") +public class ErrorResponse { + @Schema(description = "HTTP ์ƒํƒœ ์ฝ”๋“œ") private int status; + + @Schema(description = "์—๋Ÿฌ๋ช…") private String name; + + @Schema(description = "์—๋Ÿฌ ์ฝ”๋“œ") private String code; + + @Schema(description = "์—๋Ÿฌ ๋ฉ”์„ธ์ง€") private String message; - public static ResponseEntity toResponseEntity(ErrorCode e) { + public static ResponseEntity toResponseEntity(ErrorCode e) { return ResponseEntity .status(e.getStatus()) - .body(ErrorResponseDto.builder() + .body(ErrorResponse.builder() .status(e.getStatus().value()) .name(e.getName()) .code(e.getCode()) diff --git a/src/main/java/withbeetravel/dto/response/SuccessResponse.java b/src/main/java/withbeetravel/dto/response/SuccessResponse.java new file mode 100644 index 00000000..acfd3f79 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/SuccessResponse.java @@ -0,0 +1,36 @@ +package withbeetravel.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "์š”์ฒญ ์„ฑ๊ณต์— ๋Œ€ํ•œ Response DTO") +public class SuccessResponse { + + @Schema(description = "HTTP ์ƒํƒœ ์ฝ”๋“œ") + private final int status; + + @Schema(description = "์„ฑ๊ณต ๋ฉ”์„ธ์ง€") + private final String message; + + @Schema(description = "์‘๋‹ต ๋ฐ์ดํ„ฐ") + private final T data; + + public static SuccessResponse of(int status, String message, T data) { + return SuccessResponse.builder() + .status(status) + .message(message) + .data(data) + .build(); + } + + public static SuccessResponse of(int status, String message) { + return SuccessResponse.builder() + .status(status) + .message(message) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/account/AccountConnectedWibeeResponse.java b/src/main/java/withbeetravel/dto/response/account/AccountConnectedWibeeResponse.java new file mode 100644 index 00000000..17eaf45f --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/AccountConnectedWibeeResponse.java @@ -0,0 +1,10 @@ +package withbeetravel.dto.response.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AccountConnectedWibeeResponse { + private boolean isConnectedWibeeCard; +} diff --git a/src/main/java/withbeetravel/dto/response/account/AccountOwnerNameResponse.java b/src/main/java/withbeetravel/dto/response/account/AccountOwnerNameResponse.java new file mode 100644 index 00000000..d1bc07a7 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/AccountOwnerNameResponse.java @@ -0,0 +1,10 @@ +package withbeetravel.dto.response.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AccountOwnerNameResponse { + private String name; +} diff --git a/src/main/java/withbeetravel/dto/response/account/AccountResponse.java b/src/main/java/withbeetravel/dto/response/account/AccountResponse.java new file mode 100644 index 00000000..76eaf922 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/AccountResponse.java @@ -0,0 +1,26 @@ +package withbeetravel.dto.response.account; + +import lombok.Getter; +import withbeetravel.domain.Account; +import withbeetravel.domain.Product; + +@Getter +public class AccountResponse { + + private Long accountId; + private String accountNumber; + private long balance; + private Product product; + + public AccountResponse(Long accountId,String accountNumber, long balance, Product product){ + this.accountId = accountId; + this.accountNumber = accountNumber; + this.balance = balance; + this.product = product; + } + + public static AccountResponse from(Account account){ + return new AccountResponse(account.getId(), account.getAccountNumber(), account.getBalance(), account.getProduct()); + } + +} diff --git a/src/main/java/withbeetravel/dto/response/account/HistoryResponse.java b/src/main/java/withbeetravel/dto/response/account/HistoryResponse.java new file mode 100644 index 00000000..d9d33bae --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/HistoryResponse.java @@ -0,0 +1,31 @@ +package withbeetravel.dto.response.account; + +import lombok.Getter; +import withbeetravel.domain.History; + +import java.time.LocalDateTime; + +@Getter +public class HistoryResponse { + + private LocalDateTime date; + private Integer rcvAm; + private Integer payAm; + private long balance; + private String rqspeNm; + + public HistoryResponse(LocalDateTime date, Integer rcvAm, + Integer payAm, long balance, String rqspeNm) { + this.date = date; + this.rcvAm = rcvAm; + this.payAm = payAm; + this.balance = balance; + this.rqspeNm = rqspeNm; + } + + public static HistoryResponse from(History history){ + + return new HistoryResponse(history.getDate(), history.getRcvAm(), + history.getPayAM(), history.getBalance(), history.getRqspeNm()); + } +} diff --git a/src/main/java/withbeetravel/dto/response/account/PinNumberResponse.java b/src/main/java/withbeetravel/dto/response/account/PinNumberResponse.java new file mode 100644 index 00000000..f3d4a043 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/PinNumberResponse.java @@ -0,0 +1,11 @@ +package withbeetravel.dto.response.account; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PinNumberResponse { + private int failedPinCount; + private boolean pinLocked; +} diff --git a/src/main/java/withbeetravel/dto/response/account/WibeeCardHistoryListResponse.java b/src/main/java/withbeetravel/dto/response/account/WibeeCardHistoryListResponse.java new file mode 100644 index 00000000..3afc5a63 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/WibeeCardHistoryListResponse.java @@ -0,0 +1,22 @@ +package withbeetravel.dto.response.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +@Schema(description = "์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ ๋ฆฌ์ŠคํŠธ Response DTO") +public class WibeeCardHistoryListResponse { + + @Schema(description = "๊ฒฐ์ œ ๋‚ด์—ญ ์‹œ์ž‘ ๋ฒ”์œ„") + private String startDate; + + @Schema(description = "๊ฒฐ์ œ ๋‚ด์—ญ ๋ ๋ฒ”์œ„") + private String endDate; + + @Schema(description = "๊ฒฐ์ œ ๋‚ด์—ญ ๋ฆฌ์ŠคํŠธ") + private List histories; +} diff --git a/src/main/java/withbeetravel/dto/response/account/WibeeCardHistoryResponse.java b/src/main/java/withbeetravel/dto/response/account/WibeeCardHistoryResponse.java new file mode 100644 index 00000000..a3f49d5f --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/account/WibeeCardHistoryResponse.java @@ -0,0 +1,39 @@ +package withbeetravel.dto.response.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.History; + +import java.time.LocalDateTime; + +@Data +@Builder +@Schema(description = "์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ ์ •๋ณด Response DTO") +public class WibeeCardHistoryResponse { + + @Schema(description = "๊ฒฐ์ œ ๋‚ด์—ญ ID") + private Long id; + + @Schema(description = "๊ฒฐ์ œ ์‹œ๊ฐ„") + private LocalDateTime date; + + @Schema(description = "๊ฒฐ์ œ ๊ธˆ์•ก") + private int paymentAmount; + + @Schema(description = "์ƒํ˜ธ๋ช…") + private String storeName; + + @Schema(description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ ์—ฌ๋ถ€") + private boolean isAddedSharedPayment; + + public static WibeeCardHistoryResponse from (History history) { + return WibeeCardHistoryResponse.builder() + .id(history.getId()) + .date(history.getDate()) + .paymentAmount(history.getPayAM()) + .storeName(history.getRqspeNm()) + .isAddedSharedPayment(history.isAddedSharedPayment()) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/admin/DashboardResponse.java b/src/main/java/withbeetravel/dto/response/admin/DashboardResponse.java new file mode 100644 index 00000000..d8ea33b6 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/admin/DashboardResponse.java @@ -0,0 +1,13 @@ +package withbeetravel.dto.response.admin; + + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DashboardResponse { + private long loginCount; + private long totalUser; + private long totalTravel; +} diff --git a/src/main/java/withbeetravel/dto/response/admin/LoginLogResponse.java b/src/main/java/withbeetravel/dto/response/admin/LoginLogResponse.java new file mode 100644 index 00000000..769d0506 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/admin/LoginLogResponse.java @@ -0,0 +1,41 @@ +package withbeetravel.dto.response.admin; + +import lombok.Getter; +import withbeetravel.domain.LoginLog; + +@Getter +public class LoginLogResponse { + + private Long logId; + private String createdAt; + private String description; + private String ipAddress; + private String logType; + private Long userId; + + // ๊ธฐ๋ณธ ์ƒ์„ฑ์ž + public LoginLogResponse(Long logId, String createdAt, String description, String ipAddress, String logType, Long userId) { + this.logId = logId; + this.createdAt = createdAt; + this.description = description; + this.ipAddress = ipAddress; + this.logType = logType; + this.userId = userId; + } + + public static LoginLogResponse of(Long logId, String createdAt, String description, String ipAddress, String logType, Long userId) { + return new LoginLogResponse(logId, createdAt, description, ipAddress, logType, userId); + } + + public static LoginLogResponse from(LoginLog loginLog) { + return new LoginLogResponse( + loginLog.getId(), + loginLog.getCreatedAt().toString(), + loginLog.getDescription(), + loginLog.getIpAddress(), + loginLog.getLoginLogType().toString(), + loginLog.getUser().getId() + ); + } + +} diff --git a/src/main/java/withbeetravel/dto/response/admin/TravelAdminResponse.java b/src/main/java/withbeetravel/dto/response/admin/TravelAdminResponse.java new file mode 100644 index 00000000..d8b6184f --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/admin/TravelAdminResponse.java @@ -0,0 +1,26 @@ +package withbeetravel.dto.response.admin; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TravelAdminResponse { + + private Long travelId; + + private String travelName; + + private String travelType; + + private String travelStartDate; + + private String travelEndDate; + + private int totalMember; + + private Long captainId; + + private String settlementStatus; + +} diff --git a/src/main/java/withbeetravel/dto/response/admin/UserResponse.java b/src/main/java/withbeetravel/dto/response/admin/UserResponse.java new file mode 100644 index 00000000..f57e40ec --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/admin/UserResponse.java @@ -0,0 +1,32 @@ +package withbeetravel.dto.response.admin; + +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.LoginLog; +import withbeetravel.domain.User; + +import java.time.LocalDateTime; + +@Data +@Builder +public class UserResponse { + private Long userId; + private String userEmail; + private String userName; + private boolean userPinLocked; + private String userRoleType; + private String createAt; + private String recentLogin; + + public static UserResponse from(User user, LoginLog register, LoginLog loginLog){ + return new UserResponse( + user.getId(), + user.getEmail(), + user.getName(), + user.isPinLocked(), + user.getRoleType().toString(), + register.getCreatedAt().toString(), + loginLog.getCreatedAt().toString() + ); + } +} diff --git a/src/main/java/withbeetravel/dto/response/auth/AccessTokenResponse.java b/src/main/java/withbeetravel/dto/response/auth/AccessTokenResponse.java new file mode 100644 index 00000000..74136eeb --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/auth/AccessTokenResponse.java @@ -0,0 +1,16 @@ +package withbeetravel.dto.response.auth; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AccessTokenResponse { + private String accessToken; + + @Builder + public AccessTokenResponse(String accessToken) { + this.accessToken = accessToken; + } +} diff --git a/src/main/java/withbeetravel/dto/response/auth/ExpirationResponse.java b/src/main/java/withbeetravel/dto/response/auth/ExpirationResponse.java new file mode 100644 index 00000000..5c51159e --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/auth/ExpirationResponse.java @@ -0,0 +1,16 @@ +package withbeetravel.dto.response.auth; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Date; + +@Getter +@Builder +public class ExpirationResponse { + private String expirationDate; + + public static ExpirationResponse from (Date expirationDate) { + return ExpirationResponse.builder().expirationDate(expirationDate.toString()).build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/auth/MyPageResponse.java b/src/main/java/withbeetravel/dto/response/auth/MyPageResponse.java new file mode 100644 index 00000000..44f26f8a --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/auth/MyPageResponse.java @@ -0,0 +1,50 @@ +package withbeetravel.dto.response.auth; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.User; + +@Data +@Builder +@Schema(description = "MyPage ์ดˆ๊ธฐ ์ •๋ณด Response DTO") +public class MyPageResponse { + + @Schema( + description = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋ฒˆํ˜ธ", + example = "1" + ) + private int profileImage; + + @Schema( + description = "ํšŒ์› ์ด๋ฆ„", + example = "ํ™๊ธธ๋™" + ) + private String username; + + @Schema( + description = "๊ณ„์ขŒ ์ข…๋ฅ˜", + example = "WON ํ†ต์žฅ" + ) + private String accountProduct; + + @Schema( + description = "๊ณ„์ขŒ ๋ฒˆํ˜ธ", + example = "123456789012" + ) + private String accountNumber; + + public static MyPageResponse from(User user) { + return MyPageResponse.builder() + .profileImage(user.getProfileImage()) + .username(user.getName()) + .accountProduct( + user.getConnectedAccount() != null + ? user.getConnectedAccount().getProduct().name() + : null) + .accountNumber(user.getConnectedAccount() != null + ? user.getConnectedAccount().getAccountNumber() + : null) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/auth/ReissueResponse.java b/src/main/java/withbeetravel/dto/response/auth/ReissueResponse.java new file mode 100644 index 00000000..a6b5d94b --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/auth/ReissueResponse.java @@ -0,0 +1,22 @@ +package withbeetravel.dto.response.auth; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ReissueResponse { + private AccessTokenResponse accessTokenResponse; + private String refreshToken; + + @Builder + public ReissueResponse(AccessTokenResponse accessTokenResponse, String refreshToken) { + this.accessTokenResponse = accessTokenResponse; + this.refreshToken = refreshToken; + } + + public static ReissueResponse of (AccessTokenResponse accessTokenResponse, String refreshToken) { + return ReissueResponse.builder().accessTokenResponse(accessTokenResponse).refreshToken(refreshToken).build(); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/response/auth/SignInResponse.java b/src/main/java/withbeetravel/dto/response/auth/SignInResponse.java new file mode 100644 index 00000000..8c6223e4 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/auth/SignInResponse.java @@ -0,0 +1,23 @@ +package withbeetravel.dto.response.auth; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SignInResponse { + private UserAuthResponse userAuthResponse; + private String refreshToken; + + @Builder + public SignInResponse(UserAuthResponse userAuthResponse, String refreshToken) { + this.userAuthResponse = userAuthResponse; + this.refreshToken = refreshToken; + } + + public static SignInResponse of (UserAuthResponse userAuthResponse, String refreshToken) { + return SignInResponse.builder().userAuthResponse(userAuthResponse).refreshToken(refreshToken).build(); + } +} + diff --git a/src/main/java/withbeetravel/dto/response/auth/UserAuthResponse.java b/src/main/java/withbeetravel/dto/response/auth/UserAuthResponse.java new file mode 100644 index 00000000..5c58d3ba --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/auth/UserAuthResponse.java @@ -0,0 +1,23 @@ +package withbeetravel.dto.response.auth; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import withbeetravel.domain.RoleType; + +@Getter +@NoArgsConstructor +public class UserAuthResponse { + private String accessToken; + private RoleType role; + + @Builder + public UserAuthResponse(String accessToken, RoleType role) { + this.accessToken = accessToken; + this.role = role; + } + + public static UserAuthResponse of (String accessToken, RoleType role) { + return UserAuthResponse.builder().accessToken(accessToken).role(role).build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/payment/CurrencyUnitResponse.java b/src/main/java/withbeetravel/dto/response/payment/CurrencyUnitResponse.java new file mode 100644 index 00000000..2c1c082a --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/payment/CurrencyUnitResponse.java @@ -0,0 +1,19 @@ +package withbeetravel.dto.response.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +@Schema(description = "๋ฐฉ๋ฌธ ๋‚˜๋ผ ์šฐ์„  ํ†ตํ™” ์ฝ”๋“œ ์ •๋ณด Response DTO") +public class CurrencyUnitResponse { + + @Schema( + description = "ํ†ตํ™” ์ฝ”๋“œ ๋ฆฌ์ŠคํŠธ", + example = "['KRW','USD']" + ) + private List currencyUnitOptions; +} diff --git a/src/main/java/withbeetravel/dto/response/payment/SharedPaymentParticipatingMemberResponse.java b/src/main/java/withbeetravel/dto/response/payment/SharedPaymentParticipatingMemberResponse.java new file mode 100644 index 00000000..1431411e --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/payment/SharedPaymentParticipatingMemberResponse.java @@ -0,0 +1,15 @@ +package withbeetravel.dto.response.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SharedPaymentParticipatingMemberResponse { + @Schema(description = "์—ฌํ–‰ ๋ฉค๋ฒ„ ID") + private Long id; + + @Schema(description = "ํ”„๋กœํ•„ ์ด๋ฏธ์ง€") + private int profileImage; +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/response/payment/SharedPaymentRecordResponse.java b/src/main/java/withbeetravel/dto/response/payment/SharedPaymentRecordResponse.java new file mode 100644 index 00000000..8cd78012 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/payment/SharedPaymentRecordResponse.java @@ -0,0 +1,46 @@ +package withbeetravel.dto.response.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.SharedPayment; + +@Data +@Builder +@Schema(description = "SHARED PAYMENT ID์— ๋”ฐ๋ฅธ ์—ฌํ–‰ ๊ธฐ๋ก ์ •๋ณด Response DTO") +public class SharedPaymentRecordResponse { + + @Schema( + description = "SHARED PAYMENT ID", + example = "1234" + ) + private Long sharedPaymentId; + + @Schema( + description = "์—ฌํ–‰ ๊ธฐ๋ก ์ด๋ฏธ์ง€", + example = "https://~" + ) + private String paymentImage; + + @Schema( + description = "์—ฌํ–‰ ๊ธฐ๋ก ๋ฌธ๊ตฌ", + example = "์ด์–ํ˜ธ์šฐ~" + ) + private String paymentComment; + + @Schema( + description = "๋ฉ”์ธ ์—ฌํ–‰ ์‚ฌ์ง„ ์—ฌ๋ถ€", + example = "true" + ) + private boolean mainImage; + + public static SharedPaymentRecordResponse from(SharedPayment sharedPayment, boolean isMainImage) { + + return SharedPaymentRecordResponse.builder() + .sharedPaymentId(sharedPayment.getId()) + .paymentImage(sharedPayment.getPaymentImage()) + .paymentComment(sharedPayment.getPaymentComment()) + .mainImage(isMainImage) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/payment/SharedPaymentResponse.java b/src/main/java/withbeetravel/dto/response/payment/SharedPaymentResponse.java new file mode 100644 index 00000000..6eb109f4 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/payment/SharedPaymentResponse.java @@ -0,0 +1,76 @@ +package withbeetravel.dto.response.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import org.springframework.data.domain.Page; +import withbeetravel.domain.SharedPayment; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@Schema(description = "TRAVEL ID์— ๋”ฐ๋ฅธ ์—ฌํ–‰ ์ง€์ถœ ๋‚ด์—ญ ์ •๋ณด Response DTO") +public class SharedPaymentResponse { + + @Schema(description = "๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ID") + private Long id; + + @Schema(description = "๊ฒฐ์ œ ์ถ”๊ฐ€ํ•œ ์‚ฌ๋žŒ์˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€") + private int adderProfileIcon; + + @Schema(description = "๊ฒฐ์ œ ๊ธˆ์•ก") + private int paymentAmount; + + @Schema(description = "์™ธํ™” ๊ฒฐ์ œ ๊ธˆ์•ก") + private Double foreignPaymentAmount; + + @Schema(description = "ํ™˜์œจ") + private Double exchangeRate; + + @Schema(description = "ํ†ตํ™” ๋‹จ์œ„") + private String unit; + + @Schema(description = "์ƒํ˜ธ๋ช…") + private String storeName; + + @Schema(description = "์นดํ…Œ๊ณ ๋ฆฌ") + private String category; + + @Schema(description = "๋ชจ๋“  ๋ฉค๋ฒ„๊ฐ€ ์ฐธ์—ฌํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€") + private Boolean isAllMemberParticipated; + + @Schema(description = "์ •์‚ฐ์— ์ฐธ์—ฌํ•œ ์—ฌํ–‰ ๋ฉค๋ฒ„ ๋ชฉ๋ก") + private List participatingMembers; + + @Schema(description = "์ˆ˜๋™ ์ถ”๊ฐ€ ์—ฌ๋ถ€") + private Boolean isManuallyAdded; + + @Schema(description = "๊ฒฐ์ œ ์ผ์‹œ") + private LocalDateTime paymentDate; + + public static Page of( + Page sharedPayments, + int totalTravelMembers, + Map> participatingMembersMap + ) { + return sharedPayments.map(payment -> + SharedPaymentResponse.builder() + .id(payment.getId()) + .adderProfileIcon(payment.getAddedByMember().getUser().getProfileImage()) + .paymentAmount(payment.getPaymentAmount()) + .foreignPaymentAmount(payment.getForeignPaymentAmount()) + .exchangeRate(payment.getExchangeRate()) + .unit(payment.getCurrencyUnit().name()) + .category(payment.getCategory().getDescription()) + .storeName(payment.getStoreName()) + .isAllMemberParticipated(payment.getPaymentParticipatedMembers().size() == totalTravelMembers) + .participatingMembers(participatingMembersMap.get(payment.getId())) + .isManuallyAdded(payment.isManuallyAdded()) + .paymentDate(payment.getPaymentDate()) + .build() + ); + } +} diff --git a/src/main/java/withbeetravel/dto/response/settlement/MyDetailPaymentResponse.java b/src/main/java/withbeetravel/dto/response/settlement/MyDetailPaymentResponse.java new file mode 100644 index 00000000..a59d2207 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/settlement/MyDetailPaymentResponse.java @@ -0,0 +1,22 @@ +package withbeetravel.dto.response.settlement; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class MyDetailPaymentResponse { + private final int totalPaymentAmounts; + private final int totalRequestedAmounts; + private final List myDetailPaymentResponses; + + public MyDetailPaymentResponse(int totalPaymentAmounts, int totalRequestedAmounts, List myDetailPaymentResponses) { + this.totalPaymentAmounts = totalPaymentAmounts; + this.totalRequestedAmounts = totalRequestedAmounts; + this.myDetailPaymentResponses = myDetailPaymentResponses; + } + + public static MyDetailPaymentResponse of (int totalPaymentAmounts, int totalRequestedAmounts, List myDetailPaymentResponses) { + return new MyDetailPaymentResponse(totalPaymentAmounts, totalRequestedAmounts, myDetailPaymentResponses); + } +} diff --git a/src/main/java/withbeetravel/dto/response/settlement/ShowMyDetailPaymentResponse.java b/src/main/java/withbeetravel/dto/response/settlement/ShowMyDetailPaymentResponse.java new file mode 100644 index 00000000..9141b0d4 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/settlement/ShowMyDetailPaymentResponse.java @@ -0,0 +1,30 @@ +package withbeetravel.dto.response.settlement; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class ShowMyDetailPaymentResponse { + private final Long id; + private final int paymentAmount; + private final int requestedAmount; + private final String storeName; + private final LocalDateTime paymentDate; + + private ShowMyDetailPaymentResponse(Long id, + int paymentAmount, + int requestedAmount, + String storeName, + LocalDateTime paymentDate) { + this.id = id; + this.paymentAmount = paymentAmount; + this.requestedAmount = requestedAmount; + this.storeName = storeName; + this.paymentDate = paymentDate; + } + + public static ShowMyDetailPaymentResponse of (Long id, int paymentAmount, int requestedAmount, String storeName, LocalDateTime paymentDate) { + return new ShowMyDetailPaymentResponse(id, paymentAmount, requestedAmount, storeName, paymentDate); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/response/settlement/ShowMyTotalPaymentResponse.java b/src/main/java/withbeetravel/dto/response/settlement/ShowMyTotalPaymentResponse.java new file mode 100644 index 00000000..41b8f936 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/settlement/ShowMyTotalPaymentResponse.java @@ -0,0 +1,20 @@ +package withbeetravel.dto.response.settlement; + +import lombok.Getter; + +@Getter +public class ShowMyTotalPaymentResponse { + private final String name; + private final boolean isAgreed; + private final int totalPaymentCost; + + private ShowMyTotalPaymentResponse(String name, boolean isAgreed, int totalPaymentCost) { + this.name = name; + this.isAgreed = isAgreed; + this.totalPaymentCost = totalPaymentCost; + } + + public static ShowMyTotalPaymentResponse of (String name, boolean isAgreed, int ownPaymentCost, int actualBurdenCost) { + return new ShowMyTotalPaymentResponse(name, isAgreed, ownPaymentCost - actualBurdenCost); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/response/settlement/ShowOtherSettlementResponse.java b/src/main/java/withbeetravel/dto/response/settlement/ShowOtherSettlementResponse.java new file mode 100644 index 00000000..c3930ffd --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/settlement/ShowOtherSettlementResponse.java @@ -0,0 +1,22 @@ +package withbeetravel.dto.response.settlement; + +import lombok.Getter; + +@Getter +public class ShowOtherSettlementResponse { + private final Long id; + private final String name; + private final int totalPaymentCost; + private final boolean isAgreed; + + private ShowOtherSettlementResponse(Long id, String name, int totalPaymentCost, boolean isAgreed) { + this.id = id; + this.name = name; + this.totalPaymentCost = totalPaymentCost; + this.isAgreed = isAgreed; + } + + public static ShowOtherSettlementResponse of (Long id, String name, int totalPaymentCost, boolean isAgreed) { + return new ShowOtherSettlementResponse(id, name, totalPaymentCost, isAgreed); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/response/settlement/ShowSettlementDetailResponse.java b/src/main/java/withbeetravel/dto/response/settlement/ShowSettlementDetailResponse.java new file mode 100644 index 00000000..333534f4 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/settlement/ShowSettlementDetailResponse.java @@ -0,0 +1,41 @@ +package withbeetravel.dto.response.settlement; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class ShowSettlementDetailResponse { + private final ShowMyTotalPaymentResponse myTotalPayment; + private final int disagreeCount; + private final int totalPaymentAmounts; + private final int totalRequestedAmounts; + private final List myDetailPayments; + private final List others; + + private ShowSettlementDetailResponse( + ShowMyTotalPaymentResponse myTotalPayment, + int disagreeCount, + int totalPaymentAmounts, + int totalRequestedAmounts, + List myDetailPayments, + List others) { + this.myTotalPayment = myTotalPayment; + this.disagreeCount = disagreeCount; + this.totalPaymentAmounts = totalPaymentAmounts; + this.totalRequestedAmounts = totalRequestedAmounts; + this.myDetailPayments = myDetailPayments; + this.others = others; + } + + public static ShowSettlementDetailResponse of ( + ShowMyTotalPaymentResponse myTotalPayment, + int disagreeCount, + int totalPaymentAmounts, + int totalRequestedAmounts, + List myDetailPayments, + List others) { + return new ShowSettlementDetailResponse( + myTotalPayment, disagreeCount, totalPaymentAmounts, totalRequestedAmounts, myDetailPayments, others); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/dto/response/travel/HoneyCapsuleResponse.java b/src/main/java/withbeetravel/dto/response/travel/HoneyCapsuleResponse.java new file mode 100644 index 00000000..f62e71c1 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/HoneyCapsuleResponse.java @@ -0,0 +1,83 @@ +package withbeetravel.dto.response.travel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.SharedPayment; + +import java.time.LocalDateTime; + +@Data +@Builder +@Schema(description = "ํ—ˆ๋‹ˆ์บก์А Response DTO") +public class HoneyCapsuleResponse { + + @Schema( + description = "SHARED PAYMENT ID", + example = "1234" + ) + private Long sharedPaymentId; + + @Schema( + description = "๊ฒฐ์ œ ์ผ์ž", + example = "2024-11-05T14:13:00" + ) + private LocalDateTime paymentDate; + + @Schema( + description = "๊ธฐ๋ก ์‚ฌ์ง„(URL)", + example = "https://~" + ) + private String paymentImage; + + @Schema( + description = "๊ธฐ๋ก ๋ฌธ๊ตฌ", + example = "ํฌ์ผ€ ํŒŒ๋ผ๋‹ค์ด์Šค \uD83D\uDD25\uD83D\uDD25" + ) + private String paymentComment; + + @Schema( + description = "์ƒํ˜ธ๋ช…", + example = "Poke Bowls" + ) + private String storeName; + + @Schema( + description = "์›ํ™” ๊ฒฐ์ œ ๊ธˆ์•ก(nullable)", + example = "95000" + ) + private Integer paymentAmount; + + @Schema( + description = "์™ธํ™” ๊ฒฐ์ œ ๊ธˆ์•ก", + example = "95000.0" + ) + private Double foreignPaymentAmount; + + @Schema( + description = "ํ™”ํ ๋‹จ์œ„", + example = "USD" + ) + private String unit; + + @Schema( + description = "๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ ๋ฉค๋ฒ„์˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€", + example = "1" + ) + private int addMemberProfileImage; + + public static HoneyCapsuleResponse from(SharedPayment sharedPayment) { + + return HoneyCapsuleResponse.builder() + .sharedPaymentId(sharedPayment.getId()) + .paymentDate(sharedPayment.getPaymentDate()) + .paymentImage(sharedPayment.getPaymentImage()) + .paymentComment(sharedPayment.getPaymentComment()) + .storeName(sharedPayment.getStoreName()) + .paymentAmount(sharedPayment.getPaymentAmount()) + .foreignPaymentAmount(sharedPayment.getForeignPaymentAmount()) + .unit(sharedPayment.getCurrencyUnit().name()) + .addMemberProfileImage(sharedPayment.getAddedByMember().getUser().getProfileImage()) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/travel/InviteCodeGetResponse.java b/src/main/java/withbeetravel/dto/response/travel/InviteCodeGetResponse.java new file mode 100644 index 00000000..fbb171f5 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/InviteCodeGetResponse.java @@ -0,0 +1,12 @@ +package withbeetravel.dto.response.travel; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class InviteCodeGetResponse { + private String inviteCode; +} diff --git a/src/main/java/withbeetravel/dto/response/travel/InviteCodeSignUpResponse.java b/src/main/java/withbeetravel/dto/response/travel/InviteCodeSignUpResponse.java new file mode 100644 index 00000000..cb273d37 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/InviteCodeSignUpResponse.java @@ -0,0 +1,16 @@ +package withbeetravel.dto.response.travel; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class InviteCodeSignUpResponse { + private Long travelId; + + @Builder + public InviteCodeSignUpResponse(Long travelId) { + this.travelId = travelId; + } +} diff --git a/src/main/java/withbeetravel/dto/response/travel/TravelHomeResponse.java b/src/main/java/withbeetravel/dto/response/travel/TravelHomeResponse.java new file mode 100644 index 00000000..513175eb --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/TravelHomeResponse.java @@ -0,0 +1,78 @@ +package withbeetravel.dto.response.travel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.SettlementStatus; +import withbeetravel.domain.Travel; +import withbeetravel.domain.TravelMember; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@Schema(description = "์—ฌํ–‰ ํ™ˆ Response DTO") +public class TravelHomeResponse { + + @Schema(description = "์—ฌํ–‰ id") + private Long id; + + @Schema(description = "์—ฌํ–‰๋ช…") + private String travelName; + + @Schema(description = "์—ฌํ–‰์‹œ์ž‘์ผ") + private LocalDate travelStartDate; + + @Schema(description = "์—ฌํ–‰์ข…๋ฃŒ์ผ") + private LocalDate travelEndDate; + + @Schema(description = "๊ตญ๋‚ด์—ฌํ–‰์—ฌ๋ถ€") + private Boolean isDomesticTravel; + + @Schema(description = "์—ฌํ–‰์ง€ ๋ฆฌ์ŠคํŠธ") + private List countries; + + @Schema(description = "๋Œ€ํ‘œ ์ด๋ฏธ์ง€ URL") + private String mainImage; + + @Schema( + description = "์ง€์ถœ ํ•ญ๋ชฉ๋ณ„ ๋น„์œจ ํ†ต๊ณ„", + example = "{'์‹๋น„': 30.5, '์ˆ™๋ฐ•': 40.2, '๊ตํ†ต': 29.3}" + ) + private Map statistics; + + @Schema(description = "์—ฌํ–‰ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ") + private List travelMembers; + + @Schema(description = "๋ณธ์ธ์ด ๊ทธ๋ฃน์žฅ์ธ์ง€ ์—ฌ๋ถ€") + private boolean isCaptain; + + @Schema(description = "์ •์‚ฐ์ด ์ง„ํ–‰ ์ค‘์ธ์ง€ ์—ฌ๋ถ€") + private SettlementStatus settlementStatus; + + @Schema(description = "์ •์‚ฐ ๋‚ด์—ญ์— ๋™์˜ํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€") + private Boolean isAgreed; + + public static TravelHomeResponse of(Travel travel, TravelMember travelMember, Map statistics) { + return TravelHomeResponse.builder() + .id(travel.getId()) + .travelName(travel.getTravelName()) + .travelStartDate(travel.getTravelStartDate()) + .travelEndDate(travel.getTravelEndDate()) + .isDomesticTravel(travel.isDomesticTravel()) + .countries(travel.getCountries() + .stream().map(country -> country.getCountry().getCountryName()) + .toList()) + .mainImage(travel.getMainImage()) + .statistics(statistics) + .travelMembers(travel.getTravelMembers() + .stream().map(TravelMemberResponse::from) + .toList()) + .isCaptain(travelMember.isCaptain()) + .settlementStatus(travel.getSettlementStatus()) + .isAgreed((travelMember.getSettlementHistory() != null) ? travelMember.getSettlementHistory().isAgreed() : false) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/travel/TravelListResponse.java b/src/main/java/withbeetravel/dto/response/travel/TravelListResponse.java new file mode 100644 index 00000000..7518c4c5 --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/TravelListResponse.java @@ -0,0 +1,43 @@ +package withbeetravel.dto.response.travel; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import withbeetravel.domain.Travel; +import withbeetravel.domain.TravelCountry; + +import java.util.List; +import java.util.stream.Collectors; + + +@Getter +@Setter +@AllArgsConstructor +public class TravelListResponse { + private Long travelId; + private String travelName; + private String travelStartDate; + private String travelEndDate; + private String travelMainImage; + private Boolean isDomesticTravel; + private List country; + private int profileImage; + + public static TravelListResponse from(Travel travel, List travelCountries, int profileImage) { + // TravelCountry ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก์—์„œ country ์ด๋ฆ„๋งŒ ์ถ”์ถœ + List countryNames = travelCountries.stream() + .map(travelCountry -> travelCountry.getCountry().name()) // ์ˆ˜์ •: TravelCountry์—์„œ Country ๊ฐ์ฒด ์ฐธ์กฐ + .collect(Collectors.toList()); + + return new TravelListResponse( + travel.getId(), + travel.getTravelName(), + travel.getTravelStartDate().toString(), + travel.getTravelEndDate().toString(), + travel.getMainImage(), + travel.isDomesticTravel(), + countryNames, + profileImage + ); + } +} diff --git a/src/main/java/withbeetravel/dto/response/travel/TravelMemberResponse.java b/src/main/java/withbeetravel/dto/response/travel/TravelMemberResponse.java new file mode 100644 index 00000000..5441012f --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/TravelMemberResponse.java @@ -0,0 +1,21 @@ +package withbeetravel.dto.response.travel; + +import lombok.Builder; +import lombok.Data; +import withbeetravel.domain.TravelMember; + +@Data +@Builder +public class TravelMemberResponse { + private Long id; + private String name; + private int profileImage; + + public static TravelMemberResponse from(TravelMember travelMember) { + return TravelMemberResponse.builder() + .id(travelMember.getId()) + .name(travelMember.getUser().getName()) + .profileImage(travelMember.getUser().getProfileImage()) + .build(); + } +} diff --git a/src/main/java/withbeetravel/dto/response/travel/TravelResponse.java b/src/main/java/withbeetravel/dto/response/travel/TravelResponse.java new file mode 100644 index 00000000..b0d42f2a --- /dev/null +++ b/src/main/java/withbeetravel/dto/response/travel/TravelResponse.java @@ -0,0 +1,44 @@ +package withbeetravel.dto.response.travel; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import withbeetravel.domain.Travel; +import withbeetravel.domain.TravelCountry; + +import java.util.List; + +@Getter +@Setter +public class TravelResponse { + private Long travelId; + private String name; + private List country; + private String startDate; + private String endDate; + + @Builder + public TravelResponse(Long travelId, String name, List country, String startDate, String endDate) { + this.travelId = travelId; + this.name = name; + this.country = country; + this.startDate = startDate; + this.endDate = endDate; + } + + // Travel ์—”ํ„ฐํ‹ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ TravelResponseDto๋กœ ๋ณ€ํ™˜ํ•˜๋Š” from ๋ฉ”์„œ๋“œ + public static TravelResponse from(Travel travel, List travelCountries) { + // TravelCountry ์—”ํ‹ฐํ‹ฐ ๋ชฉ๋ก์—์„œ country ์ด๋ฆ„๋งŒ ์ถ”์ถœ + List countryNames = travelCountries.stream() + .map(travelCountry -> travelCountry.getCountry().name()) + .toList(); + + return new TravelResponse( + travel.getId(), + travel.getTravelName(), + countryNames, + travel.getTravelStartDate().toString(), + travel.getTravelEndDate().toString() + ); + } +} diff --git a/src/main/java/withbeetravel/exception/error/AuthErrorCode.java b/src/main/java/withbeetravel/exception/error/AuthErrorCode.java index 753928b3..5b4c9df4 100644 --- a/src/main/java/withbeetravel/exception/error/AuthErrorCode.java +++ b/src/main/java/withbeetravel/exception/error/AuthErrorCode.java @@ -15,6 +15,17 @@ public class AuthErrorCode extends ErrorCode{ public static final AuthErrorCode PASSWORD_POLICY_VIOLATION = new AuthErrorCode(HttpStatus.BAD_REQUEST, "AUTH-007", "PASSWORD_POLICY_VIOLATION", "๋น„๋ฐ€๋ฒˆํ˜ธ ์ •์ฑ… ๋ถˆ์ถฉ์กฑ"); public static final AuthErrorCode INVALID_EMAIL_FORMAT = new AuthErrorCode(HttpStatus.BAD_REQUEST, "AUTH-008", "INVALID_EMAIL_FORMAT", "์ž˜๋ชป๋œ ์ด๋ฉ”์ผ ํ˜•์‹"); public static final AuthErrorCode PIN_POLICY_VIOLATION = new AuthErrorCode(HttpStatus.BAD_REQUEST, "PIN_POLICY_VIOLATION", "AUTH-009", "ํ•€๋ฒˆํ˜ธ ์ •์ฑ… ๋ถˆ์ถฉ์กฑ"); + public static final AuthErrorCode INVALID_PASSWORD = new AuthErrorCode(HttpStatus.BAD_REQUEST, "INVALID_PASSWORD", "AUTH-010", "์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ"); + public static final AuthErrorCode INVALID_JWT = new AuthErrorCode(HttpStatus.UNAUTHORIZED, "INVALID_JWT", "AUTH-011", "์ž˜๋ชป๋œ JWT ํ† ํฐ"); + public static final AuthErrorCode EXPIRED_JWT = new AuthErrorCode(HttpStatus.UNAUTHORIZED, "EXPIRED_JWT", "AUTH-012", "๋งŒ๋ฃŒ๋œ JWT ํ† ํฐ"); + public static final AuthErrorCode UNSUPPORTED_JWT = new AuthErrorCode(HttpStatus.UNAUTHORIZED, "UNSUPPORTED_JWT", "AUTH-013", "์ง€์›๋˜์ง€ ์•Š๋Š” JWT ํ† ํฐ"); + public static final AuthErrorCode EMPTY_JWT = new AuthErrorCode(HttpStatus.BAD_REQUEST, "EMPTY_JWT", "AUTH-014", "๋นˆ JWT ํ† ํฐ"); + public static final AuthErrorCode REFRESH_TOKEN_NOT_FOUND = new AuthErrorCode(HttpStatus.NOT_FOUND, "REFRESH_TOKEN_NOT_FOUND", "AUTH-015", "๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์—†์Œ"); + public static final AuthErrorCode INVALID_REFRESH_TOKEN = new AuthErrorCode(HttpStatus.UNAUTHORIZED, "INVALID_REFRESH_TOKEN", "AUTH-016", "์ž˜๋ชป๋œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ"); + public static final AuthErrorCode USER_NOT_FOUND = new AuthErrorCode(HttpStatus.NOT_FOUND, "USER_NOT_FOUND", "AUTH-017", "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ"); + public static final AuthErrorCode VALIDATION_FAILED = new AuthErrorCode(HttpStatus.BAD_REQUEST, "VALIDATION_FAILED", "AUTH-018", "์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ"); + public static final AuthErrorCode SCHEDULER_PROCESSING_FAILED = new AuthErrorCode(HttpStatus.UNPROCESSABLE_ENTITY, "SCHEDULER_PROCESSING_FAILED", "AUTH-019", "๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์‚ญ์ œ ์Šค์ผ€์ค„๋ง ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + public static final AuthErrorCode ACCOUNT_NOT_CREATED = new AuthErrorCode(HttpStatus.UNPROCESSABLE_ENTITY, "ACCOUNT_NOT_CREATED", "AUTH-020", "ํšŒ์›๊ฐ€์ž… ์ค‘ ๊ณ„์ขŒ ์ƒ์„ฑ ์˜ค๋ฅ˜"); private AuthErrorCode(HttpStatus status, String name, String code, String message) { super(status, name, code, message); diff --git a/src/main/java/withbeetravel/exception/error/BankingErrorCode.java b/src/main/java/withbeetravel/exception/error/BankingErrorCode.java new file mode 100644 index 00000000..38a08218 --- /dev/null +++ b/src/main/java/withbeetravel/exception/error/BankingErrorCode.java @@ -0,0 +1,43 @@ +package withbeetravel.exception.error; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class BankingErrorCode extends ErrorCode{ + + public static final BankingErrorCode INSUFFICIENT_FUNDS + = new BankingErrorCode(HttpStatus.BAD_REQUEST,"INSUFFICIENT_FUNDS","BANKING-001","๊ณ„์ขŒ ์ž”์•ก ๋ถ€์กฑ"); + + public static final BankingErrorCode ACCOUNT_NOT_FOUND + = new BankingErrorCode(HttpStatus.NOT_FOUND,"ACCOUNT_NOT_FOUND","BANKING-002","์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ณ„์ขŒ๋ฒˆํ˜ธ"); + + public static final BankingErrorCode TRANSFER_LIMIT_EXCEEDED + = new BankingErrorCode(HttpStatus.BAD_REQUEST,"TRANSFER_LIMIT_EXCEEDED","BANKING-003","ํ•˜๋ฃจ ์ด์ฒด ํ•œ๋„ ์ดˆ๊ณผ"); + + public static final BankingErrorCode WIBEE_CARD_NOT_ISSUED + = new BankingErrorCode(HttpStatus.FORBIDDEN,"WIBEE_CARD_NOT_ISSUED","BANKING-004","์œ„๋น„ ์นด๋“œ๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์ง€ ์•Š์€ ํšŒ์›์ž…๋‹ˆ๋‹ค."); + + public static final BankingErrorCode HISTORY_NOT_FOUND + = new BankingErrorCode(HttpStatus.NOT_FOUND,"HISTORY_NOT_FOUND","BANKING-005","์š”์ฒญํ•˜์‹  ๊ฑฐ๋ž˜ ๋‚ด์—ญ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + public static final BankingErrorCode HISTORY_ACCESS_FORBIDDEN + = new BankingErrorCode(HttpStatus.FORBIDDEN,"HISTORY_ACCESS_FORBIDDEN","BANKING-006","ํ•ด๋‹น ๊ฑฐ๋ž˜ ๋‚ด์—ญ์˜ ์ ‘๊ทผ ๊ถŒํ•œ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"); + + public static final BankingErrorCode PAYMENT_ALREADY_EXISTS + = new BankingErrorCode(HttpStatus.NOT_FOUND,"PAYMENT_ALREADY_EXISTS","BANKING-007","์ด๋ฏธ ์ถ”๊ฐ€๋œ ๊ฒฐ์ œ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค"); + + public static final BankingErrorCode INSUFFICIENT_MANAGER_ACCOUNT_BALANCE + = new BankingErrorCode(HttpStatus.BAD_REQUEST,"INSUFFICIENT_MANAGER_ACCOUNT_BALANCE","BANKING-008","๊ด€๋ฆฌ์ž ๊ณ„์ขŒ์˜ ์ž”์•ก ๋ถ€์กฑ"); + + public static final BankingErrorCode INVALID_PIN_NUMBER + = new BankingErrorCode(HttpStatus.BAD_REQUEST, "INVALID_PIN_NUMBER", "BANKING-009", "์ž˜๋ชป๋œ ํ•€ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค"); + + public static final BankingErrorCode ACCOUNT_LOCKED + = new BankingErrorCode(HttpStatus.LOCKED, "ACCOUNT_LOCKED", "BANKING-010", "๊ณ„์ •์ด ์ž ๊ฒผ์Šต๋‹ˆ๋‹ค. PIN ์žฌ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"); + + public BankingErrorCode(HttpStatus status, + String name, String code, String message) { + super(status, name, code, message); + } +} diff --git a/src/main/java/withbeetravel/exception/error/PaymentErrorCode.java b/src/main/java/withbeetravel/exception/error/PaymentErrorCode.java index a7dac4e0..c4cfbf2b 100644 --- a/src/main/java/withbeetravel/exception/error/PaymentErrorCode.java +++ b/src/main/java/withbeetravel/exception/error/PaymentErrorCode.java @@ -8,6 +8,9 @@ public class PaymentErrorCode extends ErrorCode{ public static final PaymentErrorCode SHARED_PAYMENT_NOT_FOUND = new PaymentErrorCode(HttpStatus.NOT_FOUND, "SHARED_PAYMENT_NOT_FOUND", "PAYMENT-001", "SHARED PAYMENT ID์— ํ•ด๋‹นํ•˜๋Š” ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์ด ์—†์Œ"); public static final PaymentErrorCode NO_PERMISSION_TO_MODIFY_SHARED_PAYMENT = new PaymentErrorCode(HttpStatus.FORBIDDEN, "NO_PERMISSION_TO_MODIFY_SHARED_PAYMENT", "PAYMENT-002", "ํ•ด๋‹น ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ˆ˜์ • ๊ถŒํ•œ ์—†์Œ"); + public static final PaymentErrorCode SHARED_PAYMENT_ACCESS_FORBIDDEN = new PaymentErrorCode(HttpStatus.FORBIDDEN, "SHARED_PAYMENT_ACCESS_FORBIDDEN", "PAYMENT-003", "ํ•ด๋‹น ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ •๋ณด ์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"); + public static final PaymentErrorCode NON_TRAVEL_MEMBER_INCLUDED = new PaymentErrorCode(HttpStatus.FORBIDDEN, "NON_TRAVEL_MEMBER_INCLUDED", "PAYMENT-004", "์—ฌํ–‰ ๋ฉค๋ฒ„๊ฐ€ ์•„๋‹Œ Travel Member ID๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Œ"); + public static final PaymentErrorCode INVALID_SORT_TYPE = new PaymentErrorCode(HttpStatus.BAD_REQUEST, "INVALID_SORT_TYPE", "PAYMENT-005", "์ •๋ ฌ ๊ธฐ์ค€์€ latest ๋˜๋Š” amount๋งŒ ๊ฐ€๋Šฅ"); private PaymentErrorCode(HttpStatus status, String name, String code, String message) { super(status, name, code, message); diff --git a/src/main/java/withbeetravel/exception/error/SettlementErrorCode.java b/src/main/java/withbeetravel/exception/error/SettlementErrorCode.java index e1169dce..28688af4 100644 --- a/src/main/java/withbeetravel/exception/error/SettlementErrorCode.java +++ b/src/main/java/withbeetravel/exception/error/SettlementErrorCode.java @@ -10,8 +10,14 @@ public class SettlementErrorCode extends ErrorCode{ public static final SettlementErrorCode SETTLEMENT_ALREADY_AGREED = new SettlementErrorCode(HttpStatus.CONFLICT, "SETTLEMENT_ALREADY_AGREED", "SETTLEMENT-003", "์ด๋ฏธ ๋™์˜ํ•œ ์ •์‚ฐ"); public static final SettlementErrorCode SETTLEMENT_NOT_COMPLETED = new SettlementErrorCode(HttpStatus.CONFLICT, "SETTLEMENT_NOT_COMPLETED", "SETTLEMENT-005", "์•„์ง ์ •์‚ฐ ์™„๋ฃŒ ๋˜์ง€ ์•Š์Œ"); public static final SettlementErrorCode NO_PERMISSION_TO_MANAGE_SETTLEMENT = new SettlementErrorCode(HttpStatus.FORBIDDEN, "NO_PERMISSION_TO_MANAGE_SETTLEMENT", "SETTLEMENT-006", "์ •์‚ฐ ๊ด€๋ฆฌ ๊ถŒํ•œ ์—†์Œ"); + public static final SettlementErrorCode MEMBER_SETTLEMENT_HISTORY_NOT_FOUND = new SettlementErrorCode(HttpStatus.NOT_FOUND, "MEMBER_SETTLEMENT_HISTORY_NOT_FOUND", "SETTLEMENT-007", "TRAVELMEMBER ID์— ํ•ด๋‹นํ•˜๋Š” ์—ฌํ–‰ ๋ฉค๋ฒ„ ์ •์‚ฐ ๋‚ด์—ญ ์—†์Œ"); + public static final SettlementErrorCode SETTLEMENT_NOT_ONGOING = new SettlementErrorCode(HttpStatus.FORBIDDEN, "SETTLEMENT_NOT_ONGOING", "SETTLEMENT-008", "์ง„ํ–‰ ์ค‘์ธ ์ •์‚ฐ ์š”์ฒญ์ด ์•„๋‹˜"); + public static final SettlementErrorCode SETTLEMENT_DISAGREE_COUNT_NOT_CERTAIN = new SettlementErrorCode(HttpStatus.CONFLICT, "SETTLEMENT_DISAGREE_COUNT_NOT_CERTAIN", "SETTLEMENT-009", "์ด๋ฏธ ๋ชจ๋“  ์ •์‚ฐ์›์ด ๋™์˜ํ•œ ์ •์‚ฐ ์š”์ฒญ"); + public static final SettlementErrorCode SETTLEMENT_INSUFFICIENT_BALANCE = new SettlementErrorCode(HttpStatus.CONFLICT, "SETTLEMENT_INSUFFICIENT_BALANCE", "SETTLEMENT-010", "์—ฌํ–‰๋ฉค๋ฒ„์˜ ์ž”์•ก ๋ถ€์กฑ์œผ๋กœ ์ธํ•œ ์ •์‚ฐ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€"); + public static final SettlementErrorCode SETTLEMENT_ONGOING_ALREADY_EXISTS = new SettlementErrorCode(HttpStatus.CONFLICT, "SETTLEMENT_ONGOING_ALREADY_EXISTS", "SETTLEMENT-011", "์ง„ํ–‰์ค‘์ธ ์ •์‚ฐ ์š”์ฒญ์ด ์ด๋ฏธ ์กด์žฌ"); + public static final SettlementErrorCode SCHEDULER_PROCESSING_FAILED = new SettlementErrorCode(HttpStatus.UNPROCESSABLE_ENTITY, "SCHEDULER_PROCESSING_FAILED", "SETTLEMENT-012", "์ •์‚ฐ ์ •๋ฆฌ ์š”์ฒญ ์Šค์ผ€์ค„๋ง ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); private SettlementErrorCode(HttpStatus status, String name, String code, String message) { super(status, name, code, message); } -} +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/exception/error/TravelErrorCode.java b/src/main/java/withbeetravel/exception/error/TravelErrorCode.java index 30b377dc..79dd5994 100644 --- a/src/main/java/withbeetravel/exception/error/TravelErrorCode.java +++ b/src/main/java/withbeetravel/exception/error/TravelErrorCode.java @@ -9,6 +9,12 @@ public class TravelErrorCode extends ErrorCode{ public static final TravelErrorCode TRAVEL_NOT_FOUND = new TravelErrorCode(HttpStatus.NOT_FOUND, "TRAVEL_NOT_FOUND", "TRAVEL-001", "TRAVEL ID์— ํ•ด๋‹นํ•˜๋Š” ์—ฌํ–‰ ์ •๋ณด ์—†์Œ"); public static final TravelErrorCode TRAVEL_ACCESS_FORBIDDEN = new TravelErrorCode(HttpStatus.FORBIDDEN, "TRAVEL_ACCESS_FORBIDDEN", "TRAVEL-002", "์—ฌํ–‰ ์ •๋ณด ์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"); public static final TravelErrorCode TRAVEL_CATEGORY_NOT_FOUND = new TravelErrorCode(HttpStatus.NOT_FOUND, "TRAVEL_CATEGORY_NOT_FOUND", "TRAVEL-003", "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์นดํ…Œ๊ณ ๋ฆฌ"); + public static final TravelErrorCode TRAVEL_CAPTAIN_NOT = new TravelErrorCode(HttpStatus.NOT_FOUND, "TRAVEL_CAPTAIN_NOT", "TRAVEL-004", "์—ฌํ–‰ ์ƒ์„ฑ ๊ถŒํ•œ ์—†์Œ"); + public static final TravelErrorCode TRAVEL_INVITECODE_NOT = new TravelErrorCode(HttpStatus.NOT_FOUND, "TRAVEL_INVITECODE_NOT", "TRAVEL-005", "ํ•ด๋‹น ์ดˆ๋Œ€ ์ฝ”๋“œ ์—†์Œ"); + public static final TravelErrorCode TRAVEL_MEMBER_LIMIT = new TravelErrorCode(HttpStatus.CONFLICT, "TRAVEL_MEMBER_LIMIT", "TRAVEL-006", "์—ฌํ–‰ ๋ฉค๋ฒ„ ์ •์› ์ดˆ๊ณผ"); + public static final TravelErrorCode TRAVEL_CAPTAIN_NOT_FOUND = new TravelErrorCode(HttpStatus.NOT_FOUND, "TRAVEL_CAPTAIN_NOT_FOUND", "TRAVEL-007", "์—ฌํ–‰์žฅ ์ •๋ณด ์—†์Œ"); + public static final TravelErrorCode TRAVEL_USER_ALREADY_MEMBER = new TravelErrorCode(HttpStatus.CONFLICT, "TRAVEL_USER_ALREADY_MEMBER", "TRAVEL-008", "์ด๋ฏธ ํ•ด๋‹น ์—ฌํ–‰์— ๊ฐ€์ž…๋œ ์‚ฌ์šฉ์ž"); + private TravelErrorCode(HttpStatus status, String name, String code, String message) { super(status, name, code, message); diff --git a/src/main/java/withbeetravel/exception/error/ValidationErrorCode.java b/src/main/java/withbeetravel/exception/error/ValidationErrorCode.java index b3fe642d..91061d95 100644 --- a/src/main/java/withbeetravel/exception/error/ValidationErrorCode.java +++ b/src/main/java/withbeetravel/exception/error/ValidationErrorCode.java @@ -9,6 +9,8 @@ public class ValidationErrorCode extends ErrorCode{ public static final ValidationErrorCode MISSING_REQUIRED_FIELDS = new ValidationErrorCode(HttpStatus.BAD_REQUEST, "MISSING_REQUIRED_FIELDS", "VALIDATION-001", "ํ•„์ˆ˜ ์ •๋ณด ๋ˆ„๋ฝ"); public static final ValidationErrorCode INVALID_DATE_FORMAT = new ValidationErrorCode(HttpStatus.BAD_REQUEST, "INVALID_DATE_FORMAT", "VALIDATION-002", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋‚ ์งœ ํ˜•์‹"); public static final ValidationErrorCode DATE_RANGE_ERROR = new ValidationErrorCode(HttpStatus.BAD_REQUEST, "DATE_RANGE_ERROR", "VALIDATION-003", "๋‚ ์งœ ๋ฒ”์œ„ ์˜ค๋ฅ˜"); + public static final ValidationErrorCode IMAGE_PROCESSING_FAILED = new ValidationErrorCode(HttpStatus.UNPROCESSABLE_ENTITY, "IMAGE_PROCESSING_FAILED", "VALIDATION-004", "์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + public static final ValidationErrorCode INVALID_CURRENCY_UNIT = new ValidationErrorCode(HttpStatus.BAD_REQUEST, "INVALID_CURRENCY_UNIT", "VALIDATION-005", "์ง€์›๋˜์ง€ ์•Š๋Š” ํ†ตํ™” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค."); private ValidationErrorCode(HttpStatus status, String name, String code, String message) { super(status, name, code, message); diff --git a/src/main/java/withbeetravel/exception/handler/CustomExceptionHandler.java b/src/main/java/withbeetravel/exception/handler/CustomExceptionHandler.java index 00a7c148..6304c611 100644 --- a/src/main/java/withbeetravel/exception/handler/CustomExceptionHandler.java +++ b/src/main/java/withbeetravel/exception/handler/CustomExceptionHandler.java @@ -1,16 +1,38 @@ package withbeetravel.exception.handler; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import withbeetravel.exception.CustomException; -import withbeetravel.exception.dto.ErrorResponseDto; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.exception.error.AuthErrorCode; + @ControllerAdvice public class CustomExceptionHandler { @ExceptionHandler(CustomException.class) - protected ResponseEntity handleAuthException(CustomException e) { - return ErrorResponseDto.toResponseEntity(e.getErrorCode()); + protected ResponseEntity handleAuthException(CustomException e) { + return ErrorResponse.toResponseEntity(e.getErrorCode()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleValidationException(MethodArgumentNotValidException e) { + // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์—์„œ ์‹คํŒจํ•œ ํ•„๋“œ์™€ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”์ถœ + String errorMessages = e.getBindingResult().getFieldErrors().stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .reduce((msg1, msg2) -> msg1 + ", " + msg2) + .orElse("์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ"); + + // ErrorResponse ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์‘๋‹ต ๋ฐ˜ํ™˜ + AuthErrorCode validationFailed = AuthErrorCode.VALIDATION_FAILED; + return ResponseEntity.status(validationFailed.getStatus()) + .body(ErrorResponse.builder() + .status(validationFailed.getStatus().value()) + .name(validationFailed.getName()) + .code(validationFailed.getCode()) + .message(errorMessages) + .build()); } } \ No newline at end of file diff --git a/src/main/java/withbeetravel/jwt/JwtAuthFilter.java b/src/main/java/withbeetravel/jwt/JwtAuthFilter.java new file mode 100644 index 00000000..767c3bd3 --- /dev/null +++ b/src/main/java/withbeetravel/jwt/JwtAuthFilter.java @@ -0,0 +1,95 @@ +package withbeetravel.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; +import withbeetravel.dto.response.ErrorResponse; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.service.auth.CustomUserDetailsService; + +import java.io.IOException; +import java.util.Map; + +// jwt์˜ ๊ฒ€์ฆ ํ•„ํ„ฐ ์ˆ˜ํ–‰ +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final CustomUserDetailsService customUserDetailsService; + private final JwtUtil jwtUtil; + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + final String token = request.getHeader("Authorization"); + String userId = null; + + // Bearer Token ๊ฒ€์ฆ ํ›„ userId ์กฐํšŒ + if (token != null && !token.isEmpty()) { + String jwtToken = token.substring(7); + userId = jwtUtil.getUserNameFromToken(jwtToken); + + // ํ† ํฐ ์—๋Ÿฌ ์ฝ”๋“œ ์ฒ˜๋ฆฌ + if (handleTokenError(response, userId)) return; + } + + // token ๊ฒ€์ฆ ์™„๋ฃŒ ํ›„ SecurityContextHolder์— ๋‚ด ์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์ €์žฅ + if (userId != null && !userId.isEmpty() && SecurityContextHolder.getContext().getAuthentication() == null) { + // ์œ ์ €์™€ ํ† ํฐ ์ผ์น˜ ์‹œ userDetails ์ƒ์„ฑ + UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId); + + if (userDetails != null) { + // UserDetails, Password, Role -> ์ ‘๊ทผ ๊ถŒํ•œ ์ธ์ฆ Token ์ƒ์„ฑ + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + + // ํ˜„์žฌ request์˜ Security Context์— ์ ‘๊ทผ ๊ถŒํ•œ ์„ค์ • + SecurityContextHolder.getContext() + .setAuthentication(usernamePasswordAuthenticationToken); + } + } + filterChain.doFilter(request, response); // ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ๋„˜๊น€ + } + + private boolean handleTokenError(HttpServletResponse response, String userId) { + Map errorCodeMap = Map.of( + "EXPIRED", AuthErrorCode.EXPIRED_JWT, + "INVALID", AuthErrorCode.INVALID_JWT, + "UNSUPPORTED", AuthErrorCode.UNSUPPORTED_JWT, + "EMPTY", AuthErrorCode.EMPTY_JWT + ); + + if (errorCodeMap.containsKey(userId)) { + jwtExceptionHandler(response, errorCodeMap.get(userId)); + return true; + } + return false; + } + + public void jwtExceptionHandler(HttpServletResponse response, AuthErrorCode authErrorCode) { + try { + // ErrorResponse ์ƒ์„ฑ + ResponseEntity errorResponseEntity = ErrorResponse.toResponseEntity(authErrorCode); + + // Http ์ƒํƒœ ์ฝ”๋“œ ์„ค์ • + response.setStatus(errorResponseEntity.getStatusCodeValue()); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + // JSON์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์‘๋‹ต ๋ณธ๋ฌธ์— ์ž‘์„ฑ + ObjectMapper objectMapper = new ObjectMapper(); + String jsonResponse = objectMapper.writeValueAsString(errorResponseEntity.getBody()); + response.getWriter().write(jsonResponse); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/withbeetravel/jwt/JwtUtil.java b/src/main/java/withbeetravel/jwt/JwtUtil.java new file mode 100644 index 00000000..1fb22a6c --- /dev/null +++ b/src/main/java/withbeetravel/jwt/JwtUtil.java @@ -0,0 +1,144 @@ +package withbeetravel.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SecurityException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +// JwtUtil : jwt ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ +@Slf4j +@Component +public class JwtUtil { + + // jwt ์„œ๋ช…์— ์‚ฌ์šฉํ•  key + private final Key key; + + // ์•ก์„ธ์Šค ํ† ํฐ์˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ + private final long accessTokenExpTime; + + public JwtUtil(@Value("${jwt.secret}") final String secretKey, + @Value("${jwt.expiration_time}") final long accessTokenExpTime) { + + // secretKey๋ฅผ Base64๋กœ ๋””์ฝ”๋”ฉํ•˜์—ฌ SHA ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€ํ™˜ + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + this.accessTokenExpTime = accessTokenExpTime; + } + + // Token์—์„œ UserName ์กฐํšŒ + public String getUserNameFromToken(final String token) { + return getClaimFromToken(token, Claims::getId); + } + + // token์˜ ์‚ฌ์šฉ์ž ์†์„ฑ ์ •๋ณด ์กฐํšŒ + public T getClaimFromToken(final String token, final Function claimsResolver) { + + TokenStatus tokenStatus = isValidToken(token); + + if (tokenStatus.equals(TokenStatus.VALID)) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } else { // ํ† ํฐ ๊ด€๋ จ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๊ฒฝ์šฐ + return (T) String.valueOf(tokenStatus); + } + } + + // token ์‚ฌ์šฉ์ž ๋ชจ๋“  ์†์„ฑ ์ •๋ณด ์กฐํšŒ + private Claims getAllClaimsFromToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + // Access Token ์ƒ์„ฑ + public String generateAccessToken(final String id) { + + Map claims = new HashMap<>(); + + return Jwts.builder() + .setClaims(claims) + .setId(String.valueOf(id)) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpTime)) // 1์‹œ๊ฐ„ + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + // Refresh Token ์ƒ์„ฑ + public String generateRefreshToken(final String id) { + + long refreshTokenExpTime = 7 * 24 * 60 * 60 * 1000; + + return Jwts.builder() + .setId(String.valueOf(id)) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpTime)) // ์ผ์ฃผ์ผ + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + + // jwt ๊ฒ€์ฆ + public TokenStatus isValidToken(final String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return TokenStatus.VALID; + } catch (ExpiredJwtException e) { + log.warn("Expired JWT: {}", e.getMessage()); + return TokenStatus.EXPIRED; + } catch (SecurityException | MalformedJwtException e) { + log.warn("Invalid JWT: {}", e.getMessage()); + return TokenStatus.INVALID; + } catch (UnsupportedJwtException e) { + log.warn("Unsupported JWT: {}", e.getMessage()); + return TokenStatus.UNSUPPORTED; + } catch (IllegalArgumentException e) { + log.warn("JWT claims string is empty: {}", e.getMessage()); + return TokenStatus.EMPTY; + } + } + + public Date getExpirationDateFromToken(final String token) { + // ํ† ํฐ ์ƒํƒœ ํ™•์ธ + TokenStatus tokenStatus = isValidToken(token); + + // ๊ฐ ํ† ํฐ ์ƒํƒœ์— ๋งž๋Š” ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ธฐ + checkTokenStatus(tokenStatus); + + // ์ •์ƒ์ ์ธ ํ† ํฐ์ธ ๊ฒฝ์šฐ ๋งŒ๋ฃŒ ๋‚ ์งœ ์ถ”์ถœ + return getClaimFromToken(token, Claims::getExpiration); + } + + private void checkTokenStatus(TokenStatus tokenStatus) { + switch (tokenStatus) { + case EXPIRED: + throw new CustomException(AuthErrorCode.EXPIRED_JWT); + case INVALID: + throw new CustomException(AuthErrorCode.INVALID_JWT); + case UNSUPPORTED: + throw new CustomException(AuthErrorCode.UNSUPPORTED_JWT); + case EMPTY: + throw new CustomException(AuthErrorCode.EMPTY_JWT); + case VALID: + // ์ •์ƒ์ ์ธ ํ† ํฐ์ธ ๊ฒฝ์šฐ ๋งŒ๋ฃŒ ๋‚ ์งœ ์ถ”์ถœ + break; + } + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/jwt/TokenStatus.java b/src/main/java/withbeetravel/jwt/TokenStatus.java new file mode 100644 index 00000000..a7b5c273 --- /dev/null +++ b/src/main/java/withbeetravel/jwt/TokenStatus.java @@ -0,0 +1,9 @@ +package withbeetravel.jwt; + +public enum TokenStatus { + VALID, + EXPIRED, + INVALID, + UNSUPPORTED, + EMPTY +} diff --git a/src/main/java/withbeetravel/repository/AccountRepository.java b/src/main/java/withbeetravel/repository/AccountRepository.java new file mode 100644 index 00000000..7c510bf6 --- /dev/null +++ b/src/main/java/withbeetravel/repository/AccountRepository.java @@ -0,0 +1,21 @@ +package withbeetravel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import withbeetravel.domain.Account; +import withbeetravel.domain.User; + +import java.util.List; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + + List findByUserId(Long userId); + + Optional findByAccountNumber(String accountNumber); + + User findUserById(Long accountId); + + Optional findAccountByUserId(Long userId); + + +} diff --git a/src/main/java/withbeetravel/repository/HistoryRepository.java b/src/main/java/withbeetravel/repository/HistoryRepository.java new file mode 100644 index 00000000..d3165a54 --- /dev/null +++ b/src/main/java/withbeetravel/repository/HistoryRepository.java @@ -0,0 +1,16 @@ +package withbeetravel.repository; + + +import org.springframework.data.jpa.repository.JpaRepository; +import withbeetravel.domain.History; + +import java.time.LocalDateTime; +import java.util.List; + +public interface HistoryRepository extends JpaRepository { + List findByAccountId(Long accountId); + + List findByAccountIdOrderByDateDesc(Long accountId); + + List findByAccountIdAndDateBetween(Long accountId, LocalDateTime startDate, LocalDateTime endDate); +} diff --git a/src/main/java/withbeetravel/repository/LoginLogRepository.java b/src/main/java/withbeetravel/repository/LoginLogRepository.java new file mode 100644 index 00000000..4887b32f --- /dev/null +++ b/src/main/java/withbeetravel/repository/LoginLogRepository.java @@ -0,0 +1,31 @@ +package withbeetravel.repository; + +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 withbeetravel.domain.LoginLog; +import withbeetravel.domain.LoginLogType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface LoginLogRepository extends JpaRepository { + Page findAllByUser_Id(Long id, Pageable pageable); + + Page findAllByUser_IdAndLoginLogType(Long userId, LoginLogType loginLogType, Pageable pageable); + + @Query("SELECT COUNT(l) FROM LoginLog l WHERE l.loginLogType IN :types") + long countByLoginLogTypeIn(@Param("types") List loginLogTypes); + + @Query("SELECT l.createdAt FROM LoginLog l WHERE l.user.id = :userId ORDER BY l.createdAt ASC") + LocalDateTime findOldestLoginLogCreatedAtByUserId(@Param("userId") Long userId); + + @Query("SELECT l FROM LoginLog l WHERE l.user.id = :userId AND l.loginLogType = 'REGISTER' ORDER BY l.createdAt ASC LIMIT 1") + Optional findFirstRegisterLogByUserId(@Param("userId") Long userId); + + @Query("SELECT l FROM LoginLog l WHERE l.user.id = :userId AND l.loginLogType = 'LOGIN' ORDER BY l.createdAt DESC LIMIT 1") + Optional findMostRecentLoginLogByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/withbeetravel/repository/PaymentParticipatedMemberRepository.java b/src/main/java/withbeetravel/repository/PaymentParticipatedMemberRepository.java new file mode 100644 index 00000000..dd191f4d --- /dev/null +++ b/src/main/java/withbeetravel/repository/PaymentParticipatedMemberRepository.java @@ -0,0 +1,17 @@ +package withbeetravel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import withbeetravel.domain.PaymentParticipatedMember; + +import java.util.List; + +@Repository +public interface PaymentParticipatedMemberRepository extends JpaRepository { + + List findAllBySharedPaymentId(Long sharedPaymentId); + + List findAllByTravelMemberId(Long travelMemberId); + + boolean existsByTravelMemberIdAndSharedPaymentId(Long travelMemberId, Long sharedPaymentId); +} diff --git a/src/main/java/withbeetravel/repository/RefreshTokenRepository.java b/src/main/java/withbeetravel/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..375a341c --- /dev/null +++ b/src/main/java/withbeetravel/repository/RefreshTokenRepository.java @@ -0,0 +1,24 @@ +package withbeetravel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import withbeetravel.domain.RefreshToken; + +import java.util.Date; +import java.util.Optional; + +public interface RefreshTokenRepository extends JpaRepository { + + boolean existsByUserId(Long userId); + + @Modifying + @Query("delete from RefreshToken r where r.user.id = :userId") + void deleteByUserId(Long userId); + + Optional findByToken(String token); + + void deleteByToken(String token); + + void deleteAllByExpirationTimeBefore(Date date); +} diff --git a/src/main/java/withbeetravel/repository/SettlementRequestLogRepository.java b/src/main/java/withbeetravel/repository/SettlementRequestLogRepository.java new file mode 100644 index 00000000..4d8316f8 --- /dev/null +++ b/src/main/java/withbeetravel/repository/SettlementRequestLogRepository.java @@ -0,0 +1,11 @@ +package withbeetravel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import withbeetravel.domain.SettlementRequestLog; +import java.util.List; + +public interface SettlementRequestLogRepository extends JpaRepository { + List findAllByTravelId(Long travelId); + + List findAllByUserId(Long userId); +} diff --git a/src/main/java/withbeetravel/repository/SettlementRequestRepository.java b/src/main/java/withbeetravel/repository/SettlementRequestRepository.java new file mode 100644 index 00000000..79344e29 --- /dev/null +++ b/src/main/java/withbeetravel/repository/SettlementRequestRepository.java @@ -0,0 +1,12 @@ +package withbeetravel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import withbeetravel.domain.SettlementRequest; + +import java.util.Optional; + +public interface SettlementRequestRepository extends JpaRepository { + Optional findByTravelId(Long travelId); + + boolean existsByTravelId(Long travelId); +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/repository/SharedPaymentRepository.java b/src/main/java/withbeetravel/repository/SharedPaymentRepository.java new file mode 100644 index 00000000..3df86098 --- /dev/null +++ b/src/main/java/withbeetravel/repository/SharedPaymentRepository.java @@ -0,0 +1,45 @@ +package withbeetravel.repository; + +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 withbeetravel.domain.Category; +import withbeetravel.domain.SharedPayment; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public interface SharedPaymentRepository extends JpaRepository { + + public Optional findByIdAndTravelId(Long id, Long travelId); + + List findAllByTravelId(Long travelId); + + List findAllByAddedByMemberId(Long addedByMemberId); + + + // ๊ฒฐ์ œ ๋‚ด์—ญ ์กฐํšŒ + @Query("SELECT DISTINCT sp FROM SharedPayment sp " + + "LEFT JOIN FETCH sp.paymentParticipatedMembers ppm " + + "LEFT JOIN FETCH ppm.travelMember " + + "WHERE sp.travel.id = :travelId " + + "AND (:memberId IS NULL OR EXISTS (SELECT 1 FROM PaymentParticipatedMember pm " + + " WHERE pm.sharedPayment = sp " + + " AND pm.travelMember.id = :memberId)) " + + "AND (:startDate IS NULL OR DATE(sp.paymentDate) >= :startDate) " + + "AND (:endDate IS NULL OR DATE(sp.paymentDate) <= :endDate)" + + "AND (COALESCE(:category, sp.category) = sp.category)") + Page findAllByTravelIdAndMemberIdAndDateRange( + @Param("travelId") Long travelId, + @Param("memberId") Long memberId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("category") Category category, + Pageable pageable + ); +} diff --git a/src/main/java/withbeetravel/repository/TravelCountryRepository.java b/src/main/java/withbeetravel/repository/TravelCountryRepository.java new file mode 100644 index 00000000..93179f6b --- /dev/null +++ b/src/main/java/withbeetravel/repository/TravelCountryRepository.java @@ -0,0 +1,16 @@ +package withbeetravel.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import withbeetravel.domain.Travel; +import withbeetravel.domain.TravelCountry; + +import java.util.List; + +@Repository +public interface TravelCountryRepository extends JpaRepository { + + List findByTravelId(Long travelId); + + void deleteByTravel(Travel travel); +} diff --git a/src/main/java/withbeetravel/repository/TravelMemberRepository.java b/src/main/java/withbeetravel/repository/TravelMemberRepository.java new file mode 100644 index 00000000..58dfd78b --- /dev/null +++ b/src/main/java/withbeetravel/repository/TravelMemberRepository.java @@ -0,0 +1,26 @@ +package withbeetravel.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import withbeetravel.domain.TravelMember; +import java.util.List; + +import java.util.Optional; + +@Repository +public interface TravelMemberRepository extends JpaRepository { + + Optional findByTravelIdAndUserId(Long travelId, Long userId); + + List findAllByTravelId(Long travelId); + + int countByTravelId(Long travelId); + + List findAllByUserId(Long userId); + + boolean existsByTravelIdAndUserId(Long travelId, Long userId); + + Page findAllByUserId(Long userId, Pageable pageable); +} diff --git a/src/main/java/withbeetravel/repository/TravelMemberSettlementHistoryRepository.java b/src/main/java/withbeetravel/repository/TravelMemberSettlementHistoryRepository.java new file mode 100644 index 00000000..46dfe1f1 --- /dev/null +++ b/src/main/java/withbeetravel/repository/TravelMemberSettlementHistoryRepository.java @@ -0,0 +1,21 @@ +package withbeetravel.repository; + +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 withbeetravel.domain.TravelMemberSettlementHistory; + +import java.util.List; + +public interface TravelMemberSettlementHistoryRepository extends JpaRepository { + List findAllBySettlementRequestId(Long settlementRequestId); + TravelMemberSettlementHistory findTravelMemberSettlementHistoryBySettlementRequestIdAndTravelMemberId(Long settlementRequestId, Long travelMemberId); + + @Query("SELECT t FROM TravelMemberSettlementHistory t WHERE t.settlementRequest.id = :settlementRequestId " + + "ORDER BY (t.ownPaymentCost - t.actualBurdenCost)") + List + findAllBySettlementRequestIdOrderByCalculatedCost(@Param("settlementRequestId") Long settlementRequestId); + + void deleteAllBySettlementRequestId(Long settlementRequestId); +} diff --git a/src/main/java/withbeetravel/repository/TravelRepository.java b/src/main/java/withbeetravel/repository/TravelRepository.java new file mode 100644 index 00000000..e0217e0b --- /dev/null +++ b/src/main/java/withbeetravel/repository/TravelRepository.java @@ -0,0 +1,20 @@ +package withbeetravel.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import withbeetravel.domain.Travel; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@Repository +public interface TravelRepository extends JpaRepository { + Optional findByInviteCode(String inviteCode); + + List findAllByTravelEndDate(LocalDate localDate); + + Page findAll(Pageable pageable); +} diff --git a/src/main/java/withbeetravel/repository/UserRepository.java b/src/main/java/withbeetravel/repository/UserRepository.java new file mode 100644 index 00000000..4de19f5a --- /dev/null +++ b/src/main/java/withbeetravel/repository/UserRepository.java @@ -0,0 +1,19 @@ +package withbeetravel.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import withbeetravel.domain.User; + +import java.util.Optional; + + +public interface UserRepository extends JpaRepository { + Optional findUserByEmail(String email); + boolean existsByEmail(String email); + + Page findAll(Pageable pageable); + + Page findByNameContaining(String name, Pageable pageable); +} + diff --git a/src/main/java/withbeetravel/repository/notification/EmitterRepository.java b/src/main/java/withbeetravel/repository/notification/EmitterRepository.java new file mode 100644 index 00000000..8640bead --- /dev/null +++ b/src/main/java/withbeetravel/repository/notification/EmitterRepository.java @@ -0,0 +1,15 @@ +package withbeetravel.repository.notification; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; + +public interface EmitterRepository { + SseEmitter save(String emitterId, SseEmitter sseEmitter); + void saveEventCache(String eventCacheId, Object event); + Map findAllEmitterStartWithByUserId(String userId); + Map findAllEventCacheStartWithByUserId(String userId); + void deleteById(String id); + void deleteAllEmitterStartWithId(String userId); + void deleteAllEventCacheStartWithId(String userId); +} diff --git a/src/main/java/withbeetravel/repository/notification/EmitterRepositoryImpl.java b/src/main/java/withbeetravel/repository/notification/EmitterRepositoryImpl.java new file mode 100644 index 00000000..719b4c60 --- /dev/null +++ b/src/main/java/withbeetravel/repository/notification/EmitterRepositoryImpl.java @@ -0,0 +1,75 @@ +package withbeetravel.repository.notification; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@Repository +@Slf4j +public class EmitterRepositoryImpl implements EmitterRepository{ + private final Map emitters = new ConcurrentHashMap<>(); + private final Map eventCache = new ConcurrentHashMap<>(); + + // Emitter๋ฅผ ์ €์žฅ + @Override + public SseEmitter save(String emitterId, SseEmitter sseEmitter) { + emitters.put(emitterId, sseEmitter); + return sseEmitter; + } + + // event๋ฅผ ์ €์žฅ + @Override + public void saveEventCache(String eventCacheId, Object event) { + eventCache.put(eventCacheId, event); + } + + // ํ•ด๋‹น ํšŒ์›๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  Emitters๋ฅผ ์ฐพ์Œ + @Override + public Map findAllEmitterStartWithByUserId(String userId) { + return emitters.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(userId)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + // ํ•ด๋‹น ํšŒ์›๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  event๋ฅผ ์ฐพ์Œ + @Override + public Map findAllEventCacheStartWithByUserId(String userId) { + return eventCache.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(userId)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + // emitter๋ฅผ ์ง€์›€ + @Override + public void deleteById(String id) { + emitters.remove(id); + } + + // ํ•ด๋‹น ํšŒ์›๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  emitter๋ฅผ ์ง€์›€ + @Override + public void deleteAllEmitterStartWithId(String userId) { + emitters.forEach( + (key, emitter) -> { + if (key.startsWith(userId)) { + emitters.remove(key); + } + } + ); + } + + // ํ•ด๋‹น ํšŒ์›๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  event๋ฅผ ์ง€์›€ + @Override + public void deleteAllEventCacheStartWithId(String userId) { + eventCache.forEach( + (key, emitter) -> { + if (key.startsWith(userId)) { + eventCache.remove(key); + } + } + ); + } +} diff --git a/src/main/java/withbeetravel/scheduler/RefreshTokenScheduler.java b/src/main/java/withbeetravel/scheduler/RefreshTokenScheduler.java new file mode 100644 index 00000000..a0e0a633 --- /dev/null +++ b/src/main/java/withbeetravel/scheduler/RefreshTokenScheduler.java @@ -0,0 +1,25 @@ +package withbeetravel.scheduler; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.service.auth.AuthService; + +@Component +@RequiredArgsConstructor +public class RefreshTokenScheduler { + + private final AuthService authService; + + @Scheduled(cron = "0 0 2 * * ?", zone = "Asia/Seoul") + public void deleteExpiredToken() { + try { + authService.deleteExpiredToken(); + } catch (Exception e) { + throw new CustomException(AuthErrorCode.SCHEDULER_PROCESSING_FAILED); + } + } + +} diff --git a/src/main/java/withbeetravel/scheduler/SettlementRequestLogScheduler.java b/src/main/java/withbeetravel/scheduler/SettlementRequestLogScheduler.java new file mode 100644 index 00000000..76651cf3 --- /dev/null +++ b/src/main/java/withbeetravel/scheduler/SettlementRequestLogScheduler.java @@ -0,0 +1,26 @@ +package withbeetravel.scheduler; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.SettlementErrorCode; + +import withbeetravel.service.notification.SettlementRequestLogService; + +@Component +@RequiredArgsConstructor +public class SettlementRequestLogScheduler { + + private final SettlementRequestLogService settlementRequestLogService; + + // ๋ฉ”์ผ ์˜คํ›„ 6์‹œ์— ์‹คํ–‰ + @Scheduled(cron = "0 0 18 * * *", zone = "Asia/Seoul") + public void createSettlementLogsForEndedTravels() { + try { + settlementRequestLogService.createSettlementLogsForEndedTravels(); + } catch (Exception e) { + throw new CustomException(SettlementErrorCode.SCHEDULER_PROCESSING_FAILED); + } + } +} diff --git a/src/main/java/withbeetravel/security/CookieUtil.java b/src/main/java/withbeetravel/security/CookieUtil.java new file mode 100644 index 00000000..becf64d2 --- /dev/null +++ b/src/main/java/withbeetravel/security/CookieUtil.java @@ -0,0 +1,21 @@ +package withbeetravel.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +@RequiredArgsConstructor +public class CookieUtil { + public ResponseCookie createHttpOnlyCookie(String refreshToken) { + return ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(Duration.ofDays(30)) + .sameSite("None") + .build(); + } +} diff --git a/src/main/java/withbeetravel/security/CustomUserDetails.java b/src/main/java/withbeetravel/security/CustomUserDetails.java new file mode 100644 index 00000000..a8075b26 --- /dev/null +++ b/src/main/java/withbeetravel/security/CustomUserDetails.java @@ -0,0 +1,60 @@ +package withbeetravel.security; + +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 withbeetravel.dto.request.auth.CustomUserInfo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +// ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ์ •๋ณด ์„ค์ • +@Getter +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final CustomUserInfo user; + + @Override + public Collection getAuthorities() { + List roles = new ArrayList<>(); + roles.add("ROLE_" + user.getRole().toString()); + + return roles.stream() + .map(SimpleGrantedAuthority::new) + .toList(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/withbeetravel/security/UserAuthorizationUtil.java b/src/main/java/withbeetravel/security/UserAuthorizationUtil.java new file mode 100644 index 00000000..f3bfa78d --- /dev/null +++ b/src/main/java/withbeetravel/security/UserAuthorizationUtil.java @@ -0,0 +1,18 @@ +package withbeetravel.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +// SecurityContextHolder์—์„œ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” Util +public class UserAuthorizationUtil { + public UserAuthorizationUtil() { + throw new AssertionError(); + } + + public static Long getLoginUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + + return userDetails.getUser().getId(); + } +} diff --git a/src/main/java/withbeetravel/service/admin/AdminService.java b/src/main/java/withbeetravel/service/admin/AdminService.java new file mode 100644 index 00000000..92c8b99b --- /dev/null +++ b/src/main/java/withbeetravel/service/admin/AdminService.java @@ -0,0 +1,23 @@ +package withbeetravel.service.admin; + +import org.springframework.data.domain.Page; +import withbeetravel.domain.Travel; +import withbeetravel.dto.request.admin.TravelAdminRequest; +import withbeetravel.dto.request.admin.UserRequest; +import withbeetravel.dto.response.admin.DashboardResponse; +import withbeetravel.dto.request.admin.LoginLogRequest; +import withbeetravel.dto.response.admin.LoginLogResponse; +import withbeetravel.dto.response.admin.TravelAdminResponse; +import withbeetravel.dto.response.admin.UserResponse; + +public interface AdminService { + Page showAllLoginHistories(LoginLogRequest loginLogRequest); + + DashboardResponse getLoginAttempts(); + + Page showUsers(UserRequest userRequest); + + Page showTravels(TravelAdminRequest travelAdminRequest); + + TravelAdminResponse convertToTravelAdminResponse(Travel travel); +} diff --git a/src/main/java/withbeetravel/service/admin/AdminServiceImpl.java b/src/main/java/withbeetravel/service/admin/AdminServiceImpl.java new file mode 100644 index 00000000..467242a7 --- /dev/null +++ b/src/main/java/withbeetravel/service/admin/AdminServiceImpl.java @@ -0,0 +1,107 @@ +package withbeetravel.service.admin; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import withbeetravel.domain.*; +import withbeetravel.dto.request.admin.TravelAdminRequest; +import withbeetravel.dto.request.admin.UserRequest; +import withbeetravel.dto.response.admin.DashboardResponse; +import withbeetravel.dto.request.admin.LoginLogRequest; +import withbeetravel.dto.response.admin.LoginLogResponse; +import withbeetravel.dto.response.admin.TravelAdminResponse; +import withbeetravel.dto.response.admin.UserResponse; +import withbeetravel.repository.LoginLogRepository; +import withbeetravel.repository.TravelMemberRepository; +import withbeetravel.repository.TravelRepository; +import withbeetravel.repository.UserRepository; + +import java.util.Arrays; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AdminServiceImpl implements AdminService{ + + private final LoginLogRepository loginLogRepository; + private final UserRepository userRepository; + private final TravelRepository travelRepository; + private final TravelMemberRepository travelMemberRepository; + + public DashboardResponse getLoginAttempts() { + List loginLogTypes = Arrays.asList(LoginLogType.LOGIN, LoginLogType.LOGIN_FAILED); + + return DashboardResponse.builder().loginCount(loginLogRepository.countByLoginLogTypeIn(loginLogTypes)) + .totalUser(userRepository.count()) + .totalTravel(travelRepository.count()).build(); + } + + public Page showAllLoginHistories(LoginLogRequest loginLogRequest){ + + Pageable pageable = PageRequest.of(loginLogRequest.getPage(), loginLogRequest.getSize()); + + Page loginLogs; + + if (loginLogRequest.getLoginLogType() != null) { + loginLogs = loginLogRepository.findAllByUser_IdAndLoginLogType(loginLogRequest.getUserId(), + loginLogRequest.getLoginLogType(), pageable); + } else { + loginLogs = loginLogRepository.findAllByUser_Id(loginLogRequest.getUserId(), pageable); + } + + return loginLogs.map(LoginLogResponse::from); + } + + public Page showUsers(UserRequest userRequest) { + Pageable pageable = PageRequest.of(userRequest.getPage() - 1, userRequest.getSize()); + Page users; + + if (userRequest.getName() != null && !userRequest.getName().isEmpty()) { + users = userRepository.findByNameContaining(userRequest.getName(), pageable); + } else { + users = userRepository.findAll(pageable); + } + + return users.map(user -> { + LoginLog registerLog = loginLogRepository.findFirstRegisterLogByUserId(user.getId()) + .orElse(null); + LoginLog recentLoginLog = loginLogRepository.findMostRecentLoginLogByUserId(user.getId()) + .orElse(null); + return UserResponse.from(user, registerLog, recentLoginLog); + }); + } + + public Page showTravels(TravelAdminRequest travelAdminRequest) { + Pageable pageable = PageRequest.of(travelAdminRequest.getPage() - 1, travelAdminRequest.getSize()); + + if (travelAdminRequest.getUserId() == null) { + return travelRepository.findAll(pageable) + .map(this::convertToTravelAdminResponse); + } else { + return travelMemberRepository.findAllByUserId(travelAdminRequest.getUserId(), pageable) + .map(travelMember -> convertToTravelAdminResponse(travelMember.getTravel())); + } + } + + public TravelAdminResponse convertToTravelAdminResponse(Travel travel) { + return TravelAdminResponse.builder() + .travelId(travel.getId()) + .travelName(travel.getTravelName()) + .travelType(travel.isDomesticTravel() ? "๊ตญ๋‚ด" : "ํ•ด์™ธ") + .travelStartDate(travel.getTravelStartDate().toString()) + .travelEndDate(travel.getTravelEndDate().toString()) + .totalMember(travel.getTravelMembers().size()) + .captainId(travel.getTravelMembers().stream() + .filter(TravelMember::isCaptain) + .findFirst() + .map(travelMember -> travelMember.getUser().getId()) + .orElse(null)) + .settlementStatus(travel.getSettlementStatus().toString()) + .build(); + } + + + +} diff --git a/src/main/java/withbeetravel/service/auth/AuthService.java b/src/main/java/withbeetravel/service/auth/AuthService.java new file mode 100644 index 00000000..97f4b727 --- /dev/null +++ b/src/main/java/withbeetravel/service/auth/AuthService.java @@ -0,0 +1,23 @@ +package withbeetravel.service.auth; + +import withbeetravel.dto.request.auth.SignInRequest; +import withbeetravel.dto.request.auth.SignUpRequest; +import withbeetravel.dto.response.auth.ExpirationResponse; +import withbeetravel.dto.response.auth.MyPageResponse; +import withbeetravel.dto.response.auth.ReissueResponse; +import withbeetravel.dto.response.auth.SignInResponse; + +public interface AuthService { + void signUp(SignUpRequest signUpRequest); + SignInResponse login(SignInRequest signInRequest); + + ReissueResponse reissue(final String refreshToken); + + ExpirationResponse checkExpirationTime(final String refreshToken); + + void logout(final String refreshToken); + + MyPageResponse getMyPageInfo(Long userId); + + void deleteExpiredToken(); +} diff --git a/src/main/java/withbeetravel/service/auth/AuthServiceImpl.java b/src/main/java/withbeetravel/service/auth/AuthServiceImpl.java new file mode 100644 index 00000000..c78f919d --- /dev/null +++ b/src/main/java/withbeetravel/service/auth/AuthServiceImpl.java @@ -0,0 +1,194 @@ +package withbeetravel.service.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.*; +import withbeetravel.dto.request.account.CreateAccountRequest; +import withbeetravel.dto.request.auth.CustomUserInfo; +import withbeetravel.dto.request.auth.SignInRequest; +import withbeetravel.dto.request.auth.SignUpRequest; +import withbeetravel.dto.response.auth.*; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.jwt.JwtUtil; +import withbeetravel.jwt.TokenStatus; +import withbeetravel.repository.AccountRepository; +import withbeetravel.repository.HistoryRepository; +import withbeetravel.repository.RefreshTokenRepository; +import withbeetravel.repository.UserRepository; +import withbeetravel.service.banking.AccountService; +import withbeetravel.service.loginLog.LoginLogService; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Random; + +import static java.util.Objects.isNull; + +@Service +@RequiredArgsConstructor +@Transactional +public class AuthServiceImpl implements AuthService { + + private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + private final RefreshTokenRepository refreshTokenRepository; + private final LoginLogService loginLogService; + private final AccountService accountService; + private final AccountRepository accountRepository; + + @Override + public void signUp(SignUpRequest signUpRequest) { + if (userRepository.existsByEmail(signUpRequest.getEmail())) { + throw new CustomException(AuthErrorCode.EMAIL_ALREADY_EXISTS); + } + + User user = User.builder() + .name(signUpRequest.getName()) + .email(signUpRequest.getEmail()) + .password(bCryptPasswordEncoder.encode(signUpRequest.getPassword())) + .profileImage((int) (Math.random() * 10) + 1) + .pinNumber(signUpRequest.getPinNumber()) + .failedPinCount(0) + .pinLocked(false) + .roleType(RoleType.USER) + .build(); + userRepository.save(user); + + // ๊ณ„์ขŒ ์ƒ์„ฑ + createNewAccount(user); + + loginLogService.logRegister(user,user.getEmail()); + } + + private void createNewAccount(User user) { + accountService.createAccount(user.getId(), CreateAccountRequest.builder().product(Product.WONํ†ต์žฅ).build()); + + Account account = accountRepository.findAccountByUserId(user.getId()) + .orElseThrow(() -> new CustomException(AuthErrorCode.ACCOUNT_NOT_CREATED)); + + Random random = new Random(); + int randomBalance = random.nextInt(500_000_000) + 1; + + accountService.deposit(account.getId(), randomBalance, "์—ฌํ–‰ ์ถœ๋ฐœ ์ง€์›๊ธˆ"); + + user.updateConnectedAccount(account); + } + + @Override + public SignInResponse login(SignInRequest signInRequest) { + String email = signInRequest.getEmail(); + String password = signInRequest.getPassword(); + + User user = userRepository.findUserByEmail(email) + .orElseThrow(() -> new CustomException(AuthErrorCode.EMAIL_NOT_FOUND)); + + if (!bCryptPasswordEncoder.matches(password, user.getPassword())) { + loginLogService.logLoginFailed(user,email); // ๋กœ๊ทธ์ธ ์‹คํŒจ ๋กœ๊ทธ ์ €์žฅ + throw new CustomException(AuthErrorCode.INVALID_PASSWORD); + } + + CustomUserInfo info = CustomUserInfo.from(user); + + // jwt ํ† ํฐ ์ƒ์„ฑ + String accessToken = jwtUtil.generateAccessToken(String.valueOf(info.getId())); + + // ๊ธฐ์กด์— ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์‚ฌ์šฉ์ž์˜ refresh token ์ œ๊ฑฐ + refreshTokenRepository.deleteByUserId(info.getId()); + + // refresh token ์ƒ์„ฑ ํ›„ ์ €์žฅ + String refreshToken = jwtUtil.generateRefreshToken(String.valueOf(info.getId())); + refreshTokenRepository.save( + RefreshToken.builder() + .user(user) + .token(refreshToken) + .expirationTime(jwtUtil.getExpirationDateFromToken(refreshToken)) + .build()); + + // AccessTokenDto ์ƒ์„ฑ + UserAuthResponse userAuthResponse = UserAuthResponse.of(accessToken, user.getRoleType()); + + // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ๋กœ๊ทธ ๊ธฐ๋ก + loginLogService.logLoginSuccess(user, email); + + return SignInResponse.of(userAuthResponse, refreshToken); + } + + @Override + public ReissueResponse reissue(final String refreshToken) { + // refresh token ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + checkRefreshToken(refreshToken); + + // refresh token id ์กฐํšŒ (db์— ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ์—๋Ÿฌ ์ฒ˜๋ฆฌ) + RefreshToken foundRefreshToken = validateRefreshTokenExists(refreshToken); + Long id = foundRefreshToken.getUser().getId();; + + // ์ƒˆ๋กœ์šด Access Token ์ƒ์„ฑ + String newAccessToken = jwtUtil.generateAccessToken(String.valueOf(id)); + + // ๊ธฐ์กด์— ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์‚ฌ์šฉ์ž์˜ refresh token ์ œ๊ฑฐ + refreshTokenRepository.deleteByUserId(id); + + // ์ƒˆ๋กœ์šด refresh token ์ƒ์„ฑ ํ›„ ์ €์žฅ + String newRefreshToken = jwtUtil.generateRefreshToken(String.valueOf(id)); + Date expirationDateFromToken = jwtUtil.getExpirationDateFromToken(newRefreshToken); + User user = userRepository.findById(id).orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND)); + refreshTokenRepository.save( + RefreshToken.builder() + .user(user) + .token(newRefreshToken) + .expirationTime(expirationDateFromToken) + .build()); + + // AccessTokenDto ์ƒ์„ฑ + AccessTokenResponse accessTokenResponse = AccessTokenResponse.builder().accessToken(newAccessToken).build(); + + return ReissueResponse.of(accessTokenResponse, newRefreshToken); + } + + private RefreshToken validateRefreshTokenExists(String refreshToken) { + return refreshTokenRepository + .findByToken(refreshToken).orElseThrow( + (() -> new CustomException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND))); + } + + @Override + public ExpirationResponse checkExpirationTime(final String token) { + Date expirationDateFromToken = jwtUtil.getExpirationDateFromToken(token); + return ExpirationResponse.from(expirationDateFromToken); + } + + @Override + public void logout(String refreshToken) { + if (!isNull(refreshToken)) { + refreshTokenRepository.deleteByToken(validateRefreshTokenExists(refreshToken).getToken()); + } + } + + @Override + public MyPageResponse getMyPageInfo(Long userId) { + + User user = getUser(userId); + + return MyPageResponse.from(user); + } + + @Override + public void deleteExpiredToken() { + refreshTokenRepository.deleteAllByExpirationTimeBefore(new Date()); + } + + private void checkRefreshToken(final String refreshToken) { + if (!jwtUtil.isValidToken(refreshToken).equals(TokenStatus.VALID)) { + throw new CustomException(AuthErrorCode.INVALID_REFRESH_TOKEN); + } + } + + private User getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/service/auth/CustomUserDetailsService.java b/src/main/java/withbeetravel/service/auth/CustomUserDetailsService.java new file mode 100644 index 00000000..38a890e6 --- /dev/null +++ b/src/main/java/withbeetravel/service/auth/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package withbeetravel.service.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import withbeetravel.domain.User; +import withbeetravel.dto.request.auth.CustomUserInfo; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.repository.UserRepository; +import withbeetravel.security.CustomUserDetails; + +// ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ๋กœ๋“œํ•˜์—ฌ UserDetails ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { + Long id = Long.valueOf(userId); + User user = userRepository.findById(id).orElseThrow(() -> new CustomException(AuthErrorCode.AUTHENTICATION_FAILED)); + + CustomUserInfo customUserInfo = CustomUserInfo.from(user); + + return new CustomUserDetails(customUserInfo); + } +} diff --git a/src/main/java/withbeetravel/service/banking/AccountService.java b/src/main/java/withbeetravel/service/banking/AccountService.java new file mode 100644 index 00000000..46237102 --- /dev/null +++ b/src/main/java/withbeetravel/service/banking/AccountService.java @@ -0,0 +1,35 @@ +package withbeetravel.service.banking; + +import withbeetravel.dto.request.account.CreateAccountRequest; +import withbeetravel.dto.response.account.AccountConnectedWibeeResponse; +import withbeetravel.dto.response.account.AccountOwnerNameResponse; +import withbeetravel.dto.request.account.AccountRequest; +import withbeetravel.dto.response.account.AccountResponse; +import withbeetravel.dto.response.SuccessResponse; + +import java.util.List; + +public interface AccountService { + + List showAll(Long userId); + + AccountResponse createAccount(Long userId, CreateAccountRequest CreateAccountRequest); + + String generateUniqueAccountNumber(); + + String generateAccountNumber(); + + boolean isAccountNumberExists(String accountNumber); + + void transfer(Long accountId, String accountNumber, int amount, String rqspeNm); + + void deposit(Long accountId, int amount, String rqspeNm); + + AccountResponse accountInfo(Long accountId); + + void verifyAccount(String accountNumber); + + AccountOwnerNameResponse findUserNameByAccountNumber(String accountNumber); + + AccountConnectedWibeeResponse connectedWibee(Long accountId); +} diff --git a/src/main/java/withbeetravel/service/banking/AccountServiceImpl.java b/src/main/java/withbeetravel/service/banking/AccountServiceImpl.java new file mode 100644 index 00000000..8b8db7df --- /dev/null +++ b/src/main/java/withbeetravel/service/banking/AccountServiceImpl.java @@ -0,0 +1,225 @@ +package withbeetravel.service.banking; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.domain.Account; +import withbeetravel.domain.History; +import withbeetravel.domain.Product; +import withbeetravel.domain.User; +import withbeetravel.dto.request.account.AccountNumberRequest; +import withbeetravel.dto.request.account.CreateAccountRequest; +import withbeetravel.dto.response.account.AccountConnectedWibeeResponse; +import withbeetravel.dto.response.account.AccountOwnerNameResponse; +import withbeetravel.dto.request.account.AccountRequest; +import withbeetravel.dto.response.account.AccountOwnerNameResponse; +import withbeetravel.dto.response.account.AccountResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.BankingErrorCode; +import withbeetravel.repository.AccountRepository; +import withbeetravel.repository.HistoryRepository; +import withbeetravel.repository.UserRepository; +import withbeetravel.repository.notification.EmitterRepository; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class AccountServiceImpl implements AccountService { + + private final AccountRepository accountRepository; + private final UserRepository userRepository; + private final HistoryRepository historyRepository; + private final EmitterRepository emitterRepository; + + // ๊ณ„์ขŒ ์กฐํšŒ + public List showAll(Long userId) { + List accounts = accountRepository.findByUserId(userId); + List accountResponses = accounts.stream().map(AccountResponse::from).collect(Collectors.toList()); + return accountResponses; + } + + // TODO: ํšŒ์› ๊ณ„์ขŒ๋งŒ ์กฐํšŒ ํ•ด์•ผํ•จ + + //๊ณ„์ขŒ ์ƒ์„ฑ + @Transactional + public AccountResponse createAccount(Long userId, CreateAccountRequest createAccountRequest){ + User thisUser = userRepository.findById(userId).orElseThrow(); + + Product product = createAccountRequest.getProduct(); + + String accountNumber = generateUniqueAccountNumber(); + + Account account = Account.builder().user(thisUser) + .accountNumber(accountNumber) + .balance(0) + .product(product) + .isConnectedWibeeCard(false) + .build(); + + accountRepository.save(account); + + AccountResponse accountResponse = AccountResponse.from(account); + + return accountResponse; + } + + // ์œ ๋‹ˆํฌ ๊ณ„์ขŒ๋ฒˆํ˜ธ ํ™•์ธ + public String generateUniqueAccountNumber() { + String accountNumber; + + // ๊ณ„์ขŒ๋ฒˆํ˜ธ๊ฐ€ ์ค‘๋ณต๋˜์ง€ ์•Š๋„๋ก ๊ณ„์† ์ƒ์„ฑ + do { + accountNumber = generateAccountNumber(); + } while (isAccountNumberExists(accountNumber)); + + return accountNumber; + } + + // ๊ณ„์ขŒ๋ฒˆํ˜ธ ๋žœ๋ค ์ƒ์„ฑ + public String generateAccountNumber() { + Random random = new Random(); + StringBuilder accountNumber = new StringBuilder("1"); // ์ฒซ ๋ฒˆ์งธ ์ž๋ฆฌ๋Š” ํ•ญ์ƒ 1 + + // ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„ ๋žœ๋ค์œผ๋กœ ์ƒ์„ฑ (12์ž๋ฆฌ) + for (int i = 0; i < 12; i++) { + accountNumber.append(random.nextInt(10)); // 0-9 ์ˆซ์ž ์ƒ์„ฑ + } + + return accountNumber.toString(); + } + + // ๊ณ„์ขŒ๋ฒˆํ˜ธ ์กด์žฌ ์œ ๋ฌด ํ™•์ธ + public boolean isAccountNumberExists(String accountNumber) { + Optional existingAccount = accountRepository.findByAccountNumber(accountNumber); + return existingAccount.isPresent(); // ์กด์žฌํ•˜๋ฉด true ๋ฐ˜ํ™˜ + } + + // ์†ก๊ธˆํ•˜๊ธฐ + @Transactional + public void transfer(Long accountId, String accountNumber, int amount, String rqspeNm) { + + Account account = accountRepository.findById(accountId) + .orElseThrow(()-> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + Account targetAccount = accountRepository.findByAccountNumber(accountNumber) + .orElseThrow(()-> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + if(amount > account.getBalance()){ + throw new CustomException(BankingErrorCode.INSUFFICIENT_FUNDS); + } + + // ์ถœ๊ธˆ ์ฒ˜๋ฆฌ + + // ๊ณ„์ขŒ ๋‚ด์—ญ ๊ฐ์ฒด ์ƒ์„ฑ + History newHistory = History.builder().account(account).payAM(amount).rqspeNm(rqspeNm) + .date(LocalDateTime.now()).balance(account.getBalance()-amount).isWibeeCard(false).build(); + // ํ›„ ์ €์žฅ ์ฒ˜๋ฆฌ + historyRepository.save(newHistory); + // ํ•œ ๋‹ค์Œ ๊ณ„์ขŒ ๊ธˆ์•ก ์กฐ์ ˆ + account.transfer(-amount); + + + // ํƒ€๊ฒŸ ๊ณ„์ขŒ ์ž…๊ธˆ ์ฒ˜๋ฆฌ + + // ํƒ€๊ฒŸ ๊ณ„์ขŒ ๋‚ด์—ญ ๊ฐ์ฒด ์ƒ์„ฑ, ์ €์žฅ + // ๊ด€๋ฆฌ์ž ๊ณ„์ขŒ์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์†ก๊ธˆํ•  ๊ฒฝ์šฐ, ์†ก๊ธˆ ๋ฉ”์‹œ์ง€๋ฅผ "์œ„๋น„ํŠธ๋ž˜๋ธ” ์ •์‚ฐ๊ธˆ ์ž…๊ธˆ"์œผ๋กœ ์ง€์ • + History targetHistory; + if (account.getId() == 1L) { + targetHistory = createTargetHistory(amount, targetAccount, account, "์œ„๋น„ํŠธ๋ž˜๋ธ” ์ •์‚ฐ๊ธˆ ์ž…๊ธˆ"); + } else { + targetHistory = createTargetHistory(amount, targetAccount, account, account.getUser().getName()); + } + + historyRepository.save(targetHistory); + + // ์ƒ๋Œ€ ๊ณ„์ขŒ ์ž…๊ธˆ ์ฒ˜๋ฆฌ + targetAccount.transfer(amount); + + if(account.getId()!=1){ + sendNotification(account, amount, targetAccount);} + } + + private void sendNotification(Account senderAccount, int amount, Account targetAccount) { + Long userId = targetAccount.getId(); + String senderName = senderAccount.getUser().getName(); + String eventId = userId + "_" + System.currentTimeMillis(); + Map emitters = emitterRepository.findAllEmitterStartWithByUserId(userId.toString()); + + emitters.forEach((key, sseEmitter) -> { + Map eventData = new HashMap<>(); + eventData.put("title", "๋ˆ ๋ฐ›์•˜์–ด์š”~๐Ÿ˜Š"); + eventData.put("message", senderName + "๋‹˜์ด " + amount + "์„ ๋ณด๋ƒˆ์–ด์š”!"); + eventData.put("link", "banking/"+targetAccount.getId()); // ๊ฑฐ๋ž˜ ๋‚ด์—ญ ํŽ˜์ด์ง€๋กœ ๋งํฌ + + emitterRepository.saveEventCache(key, eventData); + try { + sseEmitter.send(SseEmitter.event().id(eventId).name("message").data(eventData)); + } catch (IOException e) { + emitterRepository.deleteById(key); + } + }); + } + + private History createTargetHistory(int amount, Account targetAccount, Account account, String rqspeNm) { + return History.builder().account(targetAccount).rcvAm(amount).rqspeNm(rqspeNm) + .date(LocalDateTime.now()).balance(targetAccount.getBalance() + amount).isWibeeCard(false).build(); + } + + @Transactional + public void deposit(Long accountId, int amount, String rqspeNm) { + Account account = accountRepository.findById(accountId) + .orElseThrow(()-> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + History history = History.builder().account(account).balance(account.getBalance()+amount).rcvAm(amount) + .date(LocalDateTime.now()).rqspeNm(rqspeNm).isWibeeCard(false).build(); + + historyRepository.save(history); + + account.transfer(amount); + + } + + // accountId๋กœ ๊ณ„์ขŒ ์กฐํšŒํ•˜๊ธฐ + public AccountResponse accountInfo(Long accountId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(()->new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + return AccountResponse.from(account); + + } + + public void verifyAccount(String accountNumber) { + Optional account = accountRepository.findByAccountNumber(accountNumber); + if(account.isEmpty()){ + throw new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND); + + } + + } + + public AccountOwnerNameResponse findUserNameByAccountNumber(String accountNumber) { + Account account = accountRepository.findByAccountNumber(accountNumber) + .orElseThrow(()-> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + String name = account.getUser().getName(); + AccountOwnerNameResponse accountOwnerNameResponse = new AccountOwnerNameResponse(name); + + return accountOwnerNameResponse; + } + + public AccountConnectedWibeeResponse connectedWibee(Long accountId){ + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + AccountConnectedWibeeResponse accountConnectedWibeeResponse + = new AccountConnectedWibeeResponse(account.isConnectedWibeeCard()); + + return accountConnectedWibeeResponse; + } +} + diff --git a/src/main/java/withbeetravel/service/banking/HistoryService.java b/src/main/java/withbeetravel/service/banking/HistoryService.java new file mode 100644 index 00000000..7289f4fa --- /dev/null +++ b/src/main/java/withbeetravel/service/banking/HistoryService.java @@ -0,0 +1,16 @@ +package withbeetravel.service.banking; + +import withbeetravel.dto.request.account.HistoryRequest; +import withbeetravel.dto.response.account.HistoryResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.account.WibeeCardHistoryListResponse; + +import java.util.List; + +public interface HistoryService { + List showAll(Long AccountId); + + void addHistory(Long userId, Long accountId, HistoryRequest historyRequest); + + WibeeCardHistoryListResponse getWibeeCardHistory(Long userId, String startDate, String endDate); +} diff --git a/src/main/java/withbeetravel/service/banking/HistoryServiceImpl.java b/src/main/java/withbeetravel/service/banking/HistoryServiceImpl.java new file mode 100644 index 00000000..115017e0 --- /dev/null +++ b/src/main/java/withbeetravel/service/banking/HistoryServiceImpl.java @@ -0,0 +1,202 @@ +package withbeetravel.service.banking; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.domain.*; +import withbeetravel.dto.request.account.HistoryRequest; +import withbeetravel.dto.response.account.HistoryResponse; +import withbeetravel.dto.response.account.WibeeCardHistoryListResponse; +import withbeetravel.dto.response.account.WibeeCardHistoryResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.exception.error.BankingErrorCode; +import withbeetravel.exception.error.ValidationErrorCode; +import withbeetravel.repository.*; +import withbeetravel.repository.notification.EmitterRepository; +import withbeetravel.service.payment.SharedPaymentRegisterService; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class HistoryServiceImpl implements HistoryService { + + private final UserRepository userRepository; + private final HistoryRepository historyRepository; + private final AccountRepository accountRepository; + private final TravelMemberRepository travelMemberRepository; + private final SharedPaymentRegisterService sharedPaymentRegisterService; + private final EmitterRepository emitterRepository; + + + public List showAll(Long accountId) { + List histories = historyRepository.findByAccountIdOrderByDateDesc(accountId); + List historyResponses = histories.stream().map(HistoryResponse::from).toList(); + return historyResponses; + } + + // ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ถ”๊ฐ€ํ•˜๊ธฐ + @Override + @Transactional + public void addHistory( + Long userId, + Long accountId, + HistoryRequest historyRequest + ){ + + Account account = accountRepository.findById(accountId) + .orElseThrow(()-> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + if(!account.isConnectedWibeeCard()){ + if(historyRequest.isWibeeCard()){ //์œ„๋น„ ์นด๋“œ ์—ฐ๊ฒฐ๋˜์–ด์žˆ์ง€ ์•Š์•˜์„ ๋•Œ, ์œ„๋น„ ์นด๋“œ๋กœ๊ฒฐ์ œํ–ˆ๋‹คํ•˜๋ฉด ์˜ค๋ฅ˜ + throw new CustomException(BankingErrorCode.WIBEE_CARD_NOT_ISSUED); + } + } + + if(account.getBalance()< historyRequest.getPayAm()){ + throw new CustomException(BankingErrorCode.INSUFFICIENT_FUNDS); + } + + History history = History.builder(). + account(account) + .date(LocalDateTime.now()) + .payAM(historyRequest.getPayAm()) + .rqspeNm(historyRequest.getRqspeNm()) + .isWibeeCard(historyRequest.isWibeeCard()) + .balance(account.getBalance()- historyRequest.getPayAm()) + .build(); + + historyRepository.save(history); + + account.transfer(-historyRequest.getPayAm()); + + if(account.isConnectedWibeeCard() && historyRequest.isWibeeCard()) { + // ์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ & ์—ฌํ–‰ ๊ธฐ๊ฐ„ ์ค‘ ๋ฐœ์ƒํ•œ ๊ฒฐ์ œ ๋‚ด์—ญ์ด๋ฉด ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์— ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ + List invitedTravelList = getInvitedTravelList(userId); // ์ฐธ์—ฌ ์ค‘์ธ ์—ฌํ–‰ ๋ฆฌ์ŠคํŠธ + + Travel currentTravel = getCurrentTravels(invitedTravelList); // ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์—ฌํ–‰ + + if (currentTravel != null) { // ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์—ฌํ–‰์ด ์žˆ๋‹ค๋ฉด, ํ•ด๋‹น ์—ฌํ–‰์˜ ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์— ํ˜„์žฌ ๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ + + sharedPaymentRegisterService.saveWibeeCardSharedPayment( + getTravelMember(userId, currentTravel.getId()), + currentTravel, + history + ); + history.addedSharedPayment(); + } + } + sendNotification(account,history); + } + + private void sendNotification(Account account,History history) { + Long userId = account.getUser().getId(); + String eventId = userId + "_" + System.currentTimeMillis(); + Map emitters = emitterRepository.findAllEmitterStartWithByUserId(userId.toString()); + String name = account.getUser().getName(); + String payDetail = history.getRqspeNm(); + int payAmount = history.getPayAM(); + + emitters.forEach((key, sseEmitter) -> { + Map eventData = new HashMap<>(); + eventData.put("title", "๊ฒฐ์ œ ์•Œ๋ฆผโœ”"); + eventData.put("message", name+"๋‹˜ "+ payDetail+"์—์„œ " + + payAmount+"์›์ด ๊ฒฐ์ œ๋˜์—ˆ์–ด์š”!๐Ÿ’ฒ"); + eventData.put("link", "banking/"+account.getId()); // ๊ฑฐ๋ž˜ ๋‚ด์—ญ ํŽ˜์ด์ง€๋กœ ๋งํฌ + + emitterRepository.saveEventCache(key, eventData); + try { + sseEmitter.send(SseEmitter.event().id(eventId).name("message").data(eventData)); + } catch (IOException e) { + emitterRepository.deleteById(key); + } + }); + } + + @Override + @Transactional(readOnly = true) + public WibeeCardHistoryListResponse getWibeeCardHistory(Long userId, String startDate, String endDate) { + + // User ์—”ํ‹ฐํ‹ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + User user = getUser(userId); + + // ์œ„๋น„ ์นด๋“œ๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์ง€ ์•Š์€ ํšŒ์›์ธ ๊ฒฝ์šฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + Account account = user.getWibeeCardAccount(); + if(account == null) + throw new CustomException(BankingErrorCode.WIBEE_CARD_NOT_ISSUED); + + // ๋‚ ์งœ ํ•„ํ„ฐ๋ง์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํ˜„์žฌ ์‹œ์ ์œผ๋กœ๋ถ€ํ„ฐ 1๋‹ฌ + LocalDate eDate = LocalDate.now(); + LocalDate sDate = eDate.minusMonths(1); + + // ๋‚ ์งœ ํ•„ํ„ฐ๋ง ์ •๋ณด๊ฐ€ ๋“ค์–ด์™”์„ ๊ฒฝ์šฐ, ๋ฒ”์œ„ ์ฒดํฌ ํ›„ ์ ์šฉ + if((startDate != null && !startDate.isEmpty()) && (endDate != null && !endDate.isEmpty())) { + validateDateRange(LocalDate.parse(startDate), LocalDate.parse(endDate)); + sDate = LocalDate.parse(startDate); + eDate = LocalDate.parse(endDate); + } + + // ๊ฒฐ์ œ ๋‚ด์—ญ ๊ฐ€์ ธ์˜ค๊ธฐ + List histories = historyRepository.findByAccountIdAndDateBetween(account.getId(), sDate.atStartOfDay(), eDate.atStartOfDay().plusDays(1)); + + // ์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ(๋‚ ์งœ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ) + List filteredHistories = histories.stream() + .filter(History::isWibeeCard) + .sorted((h1, h2) -> h1.getDate().compareTo(h2.getDate())) + .toList(); + + return WibeeCardHistoryListResponse.builder() + .startDate(sDate.toString()) + .endDate(eDate.toString()) + .histories(filteredHistories.stream() + .map(WibeeCardHistoryResponse::from) + .toList()) + .build(); + } + + + User getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND)); + } + + TravelMember getTravelMember(Long userId, Long travelId) { + return travelMemberRepository.findByTravelIdAndUserId(travelId, userId).get(); + } + + // userId์— ํ•ด๋‹นํ•˜๋Š” ํšŒ์›์ด ์ฐธ์—ฌ ์ค‘์ธ ์—ฌํ–‰ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜ + List getInvitedTravelList(Long userId) { + List travelMembers = travelMemberRepository.findAllByUserId(userId); + + // TravelMember์—์„œ Travel๋งŒ ์ถ”์ถœํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ + return travelMembers.stream() + .map(TravelMember::getTravel) // TravelMember์˜ Travel ํ•„๋“œ ์ถ”์ถœ + .toList(); // List๋กœ ๋ณ€ํ™˜ + } + + // ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์—ฌํ–‰ ๋ฐ˜ํ™˜ + public Travel getCurrentTravels(List travels) { + LocalDate today = LocalDate.now(); + + for (Travel travel : travels) { + if (!today.isBefore(travel.getTravelStartDate()) && !today.isAfter(travel.getTravelEndDate())) { + return travel; // ์กฐ๊ฑด์— ๋งž๋Š” ์ฒซ ๋ฒˆ์งธ ์—ฌํ–‰์„ ๋ฐ˜ํ™˜ + } + } + + return null; + } + + void validateDateRange(LocalDate sDate, LocalDate eDate) { + if (sDate.isAfter(eDate)) { + throw new CustomException(ValidationErrorCode.DATE_RANGE_ERROR); + } + } +} diff --git a/src/main/java/withbeetravel/service/banking/VerifyService.java b/src/main/java/withbeetravel/service/banking/VerifyService.java new file mode 100644 index 00000000..6c460eea --- /dev/null +++ b/src/main/java/withbeetravel/service/banking/VerifyService.java @@ -0,0 +1,9 @@ +package withbeetravel.service.banking; + +import withbeetravel.dto.response.account.PinNumberResponse; + +public interface VerifyService { + public void verifyPin(String pin); + + public PinNumberResponse verifyUser(); +} diff --git a/src/main/java/withbeetravel/service/banking/VerifyServiceImpl.java b/src/main/java/withbeetravel/service/banking/VerifyServiceImpl.java new file mode 100644 index 00000000..91d23136 --- /dev/null +++ b/src/main/java/withbeetravel/service/banking/VerifyServiceImpl.java @@ -0,0 +1,57 @@ +package withbeetravel.service.banking; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.User; +import withbeetravel.dto.response.account.PinNumberResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.exception.error.BankingErrorCode; +import withbeetravel.repository.UserRepository; +import withbeetravel.security.UserAuthorizationUtil; + +@Service +@RequiredArgsConstructor +public class VerifyServiceImpl implements VerifyService{ + + private final UserRepository userRepository; + + + public void verifyPin(String pin){ + Long userId = UserAuthorizationUtil.getLoginUserId(); + User user = userRepository.findById(userId) + .orElseThrow(()-> new CustomException(AuthErrorCode.AUTHENTICATION_FAILED)); + + if (user.isPinLocked()) { + throw new CustomException(BankingErrorCode.ACCOUNT_LOCKED); + } + + if (!user.validatePin(pin)) { + user.incrementFailedPinCount(); + saveUser(user); // ์ €์žฅ ํ›„ ์˜ˆ์™ธ ๋˜์ง€๊ธฐ + throw new CustomException(BankingErrorCode.INVALID_PIN_NUMBER); + } + + user.resetFailedPinCount(); + userRepository.save(user); + } + + // ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์ €์žฅ๋˜์•ผํ•จ ์œ ์ € ์‹คํŒจ ํšŸ์ˆ˜๋ฅผ ๋Š˜๋ฆฌ๊ณ  ์ €์žฅ + @Transactional + private void saveUser(User user) { + userRepository.save(user); + } + + public PinNumberResponse verifyUser(){ + Long userId = UserAuthorizationUtil.getLoginUserId(); + User user = userRepository.findById(userId) + .orElseThrow(()-> new CustomException(AuthErrorCode.AUTHENTICATION_FAILED)); + + if(user.isPinLocked()){ + throw new CustomException(BankingErrorCode.ACCOUNT_LOCKED); + } + return new PinNumberResponse(user.getFailedPinCount(), user.isPinLocked()); + } + +} diff --git a/src/main/java/withbeetravel/service/global/S3Uploader.java b/src/main/java/withbeetravel/service/global/S3Uploader.java new file mode 100644 index 00000000..9a80c440 --- /dev/null +++ b/src/main/java/withbeetravel/service/global/S3Uploader.java @@ -0,0 +1,82 @@ +package withbeetravel.service.global; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +@Service +public class S3Uploader { + + private final AmazonS3 amazonS3; + private final String bucket; + + @Value("${cloud.aws.s3.bucket.domain}") + private String bucketDomain; + + public S3Uploader(AmazonS3 amazonS3, @Value("${cloud.aws.s3.bucket}") String bucket) { + this.amazonS3 = amazonS3; + this.bucket = bucket; + } + + // ์ด๋ฏธ์ง€ ์ €์žฅ + // file := s3์— ์ €์žฅํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ + // dirName := ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ €์žฅํ•  s3 ๋””๋ ‰ํ† ๋ฆฌ + public String upload(MultipartFile file, String dirName) throws IOException { + + // ํŒŒ์ผ์˜ ์›๋ž˜ ์ด๋ฆ„์—์„œ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐ + String originalFileName = file.getOriginalFilename().replaceAll("\\s", "_"); + + // ์œ ๋‹ˆํฌํ•œ ํŒŒ์ผ๋ช…์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด UUID๋ฅผ ํŒŒ์ผ๋ช…์— ์ถ”๊ฐ€ + String uuid = UUID.randomUUID().toString(); + String uniqueFileName = uuid + "_" + originalFileName; + + // ๋””๋ ‰ํ† ๋ฆฌ ์œ„์น˜์™€ ํŒŒ์ผ๋ช… ํ•ฉ์น˜๊ธฐ + String fileName = dirName + "/" + uniqueFileName; + + // S3์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ + String contentType = file.getContentType(); + if(contentType == null) contentType = "application/octet-stream"; + String uploadImageUrl = putS3(file.getInputStream(), fileName, file.getSize(), contentType); + + // S3์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€์˜ URL ๋ฆฌํ„ด + return uploadImageUrl; + } + + // ์ด๋ฏธ์ง€ ์‚ญ์ œ + // filName := ์‚ญ์ œํ•  ์ด๋ฏธ์ง€๋ช…(URL ํ˜•์‹) + public void delete(String fileName) { + if(fileName.startsWith(bucketDomain)) { + amazonS3.deleteObject(bucket, fileName.substring(bucketDomain.length())); + } + } + + // ์ด๋ฏธ์ง€ ์ˆ˜์ • + // newFile := ์ƒˆ๋กœ ์ €์žฅํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ + // oldFileName := ๊ธฐ์กด์— ์ €์žฅ๋˜์–ด ์žˆ๋˜ ์ด๋ฏธ์ง€๋ช…(URL ํ˜•์‹) + // dirName := ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ €์žฅํ•  s3 ๋””๋ ‰ํ† ๋ฆฌ + public String update(MultipartFile newFile, String oldFileName, String dirName) throws IOException { + // ๊ธฐ์กด ํŒŒ์ผ ์‚ญ์ œ + delete(oldFileName); + // ์ƒˆ ํŒŒ์ผ ์—…๋กœ๋“œ + return upload(newFile, dirName); + } + + // S3์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ + private String putS3(InputStream inputStream, String fileName, long contentLength, String contentType) { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(contentLength); + metadata.setContentType(contentType); // Content-Type ์„ค์ • + + amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, metadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return amazonS3.getUrl(bucket, fileName).toString(); + } +} diff --git a/src/main/java/withbeetravel/service/loginLog/LoginLogService.java b/src/main/java/withbeetravel/service/loginLog/LoginLogService.java new file mode 100644 index 00000000..8353f83c --- /dev/null +++ b/src/main/java/withbeetravel/service/loginLog/LoginLogService.java @@ -0,0 +1,12 @@ +package withbeetravel.service.loginLog; + +import withbeetravel.domain.User; + +public interface LoginLogService { + + void logRegister(User user, String email); + + void logLoginSuccess(User user, String email); + + void logLoginFailed(User user, String email); +} diff --git a/src/main/java/withbeetravel/service/loginLog/LoginLogServiceImpl.java b/src/main/java/withbeetravel/service/loginLog/LoginLogServiceImpl.java new file mode 100644 index 00000000..da25393d --- /dev/null +++ b/src/main/java/withbeetravel/service/loginLog/LoginLogServiceImpl.java @@ -0,0 +1,52 @@ +package withbeetravel.service.loginLog; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.LoginLog; +import withbeetravel.domain.LoginLogType; +import withbeetravel.domain.User; +import withbeetravel.repository.LoginLogRepository; + +@Service +@RequiredArgsConstructor +@EnableAsync +public class LoginLogServiceImpl implements LoginLogService { + private final LoginLogRepository loginLogRepository; + + @Async + public void logRegister(User user, String email){ + LoginLog registerLoginLog = LoginLog.builder() + .loginLogType(LoginLogType.REGISTER) + .user(user) + .description("ํšŒ์› ๊ฐ€์ž… ์™„๋ฃŒ") + .ipAddress(email) + .build(); + loginLogRepository.save(registerLoginLog); + } + + @Async + public void logLoginSuccess(User user, String email) { + LoginLog loginLog = LoginLog.builder() + .loginLogType(LoginLogType.LOGIN) + .user(user) + .description("๋กœ๊ทธ์ธ ์„ฑ๊ณต") + .ipAddress(email) + .build(); + loginLogRepository.save(loginLog); + } + + @Async + @Transactional + public void logLoginFailed(User user, String email) { + LoginLog loginFailedLoginLog = LoginLog.builder() + .loginLogType(LoginLogType.LOGIN_FAILED) + .user(user) + .description("๋กœ๊ทธ์ธ ์‹คํŒจ - ์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ") + .ipAddress(email) // ์ด๋ฉ”์ผ์„ IP๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ IP๋ฅผ ๋ฐ›์•„์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + .build(); + loginLogRepository.save(loginFailedLoginLog); + } +} diff --git a/src/main/java/withbeetravel/service/notification/NotificationService.java b/src/main/java/withbeetravel/service/notification/NotificationService.java new file mode 100644 index 00000000..8d676626 --- /dev/null +++ b/src/main/java/withbeetravel/service/notification/NotificationService.java @@ -0,0 +1,8 @@ +package withbeetravel.service.notification; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface NotificationService { + + SseEmitter subscribe(Long userId, String lastEventId); +} diff --git a/src/main/java/withbeetravel/service/notification/NotificationServiceImpl.java b/src/main/java/withbeetravel/service/notification/NotificationServiceImpl.java new file mode 100644 index 00000000..6090f43b --- /dev/null +++ b/src/main/java/withbeetravel/service/notification/NotificationServiceImpl.java @@ -0,0 +1,75 @@ +package withbeetravel.service.notification; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.repository.notification.EmitterRepository; + +import java.io.IOException; +import java.util.Map; + +@Service +@Slf4j +@RequiredArgsConstructor +public class NotificationServiceImpl implements NotificationService{ + + private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; //1์‹œ๊ฐ„ + private final EmitterRepository emitterRepository; + + @Override + public SseEmitter subscribe(Long userId, String lastEventId) { + + // SseEmitter๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•œ ๊ณ ์œ  id ์ƒ์„ฑ + String emitterId = makeTimeIncludeId(userId); + + // ํ˜„์žฌ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์œ„ํ•œ sseEmitter ๊ฐ์ฒด ์ƒ์„ฑ + SseEmitter sseEmitter = new SseEmitter(DEFAULT_TIMEOUT); + // emitterId๋ฅผ ํ‚ค๋กœ ์‚ฌ์šฉํ•ด sseEmitter๋ฅผ ์ €์žฅ + emitterRepository.save(emitterId, sseEmitter); + + // sseEmitter ์—ฐ๊ฒฐ์ด ์™„๋ฃŒ๋  ๊ฒฝ์šฐ + sseEmitter.onCompletion(() -> emitterRepository.deleteById(emitterId)); + // sseEmitter ์—ฐ๊ฒฐ์— ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ + sseEmitter.onTimeout(() -> emitterRepository.deleteById(emitterId)); + + // 503 ์—๋Ÿฌ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ „์†ก + String eventId = makeTimeIncludeId(userId); + sendNotification(sseEmitter, eventId, emitterId, "EventStream ์ƒ์„ฑ ์™„๋ฃŒ. [userId = " + userId + "]"); + + // ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฏธ์ˆ˜์‹ ํ•œ Event ๋ชฉ๋ก์ด ์กด์žฌํ•  ๊ฒฝ์šฐ ์ „์†กํ•˜์—ฌ Event ์œ ์‹ค์„ ์˜ˆ๋ฐฉ + if (hasLostData(lastEventId)) { + sendLostData(lastEventId, userId, emitterId, sseEmitter); + } + + return sseEmitter; + } + + private boolean hasLostData(String lastEventId) { + return !lastEventId.isEmpty(); + } + + private void sendLostData(String lastEventId, Long userId, String emitterId, SseEmitter sseEmitter) { + Map eventCaches = emitterRepository.findAllEventCacheStartWithByUserId(String.valueOf(userId)); + eventCaches.entrySet().stream() + .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0) + .forEach(entry -> sendNotification(sseEmitter, entry.getKey(), emitterId, entry.getValue())); + } + + private void sendNotification(SseEmitter sseEmitter, String eventId, String emitterId, Object data) { + try { + sseEmitter.send(SseEmitter.event() + .id(eventId) + .name("connect") + .data(data)); + } catch (IOException e) { + emitterRepository.deleteById(emitterId); + } + } + + @NotNull + private String makeTimeIncludeId(Long userId) { + return userId + "_" + System.currentTimeMillis(); + } +} diff --git a/src/main/java/withbeetravel/service/notification/SettlementRequestLogService.java b/src/main/java/withbeetravel/service/notification/SettlementRequestLogService.java new file mode 100644 index 00000000..d148094a --- /dev/null +++ b/src/main/java/withbeetravel/service/notification/SettlementRequestLogService.java @@ -0,0 +1,15 @@ +package withbeetravel.service.notification; + +import withbeetravel.domain.SettlementRequest; +import withbeetravel.dto.request.settlementRequestLog.SettlementRequestLogDto; + +import java.util.List; + +public interface SettlementRequestLogService { + List getSettlementRequestLogs (Long userId); + + void createSettlementLogsForEndedTravels(); + + void createSettlementReRequestLogForNotAgreed(SettlementRequest settlementRequest); + +} diff --git a/src/main/java/withbeetravel/service/notification/SettlementRequestLogServiceImpl.java b/src/main/java/withbeetravel/service/notification/SettlementRequestLogServiceImpl.java new file mode 100644 index 00000000..b964170a --- /dev/null +++ b/src/main/java/withbeetravel/service/notification/SettlementRequestLogServiceImpl.java @@ -0,0 +1,123 @@ +package withbeetravel.service.notification; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.domain.*; +import withbeetravel.dto.request.settlementRequestLog.SettlementRequestLogDto; +import withbeetravel.repository.*; +import withbeetravel.repository.notification.EmitterRepository; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class SettlementRequestLogServiceImpl implements SettlementRequestLogService{ + + private final SettlementRequestLogRepository settlementRequestLogRepository; + private final SettlementRequestRepository settlementRequestRepository; + private final TravelRepository travelRepository; + private final TravelMemberRepository travelMemberRepository; + private final TravelMemberSettlementHistoryRepository travelMemberSettlementHistoryRepository; + private final EmitterRepository emitterRepository; + + + @Override + public List getSettlementRequestLogs(Long userId) { + + // ๋‚˜์˜ ๋ชจ๋“  ๋กœ๊ทธ ๋ฆฌ์ŠคํŠธ + List settlementRequestLogs = getSettlementRequestLogsByUserId(userId); + + // settlementRequestLogDto ๋ฆฌ์ŠคํŠธ๋กœ ๊ฐ€๊ณต (๋งํฌ ์ถ”๊ฐ€) + List settlementRequestLogDtos = new ArrayList<>(); + for (SettlementRequestLog settlementRequestLog : settlementRequestLogs) { + String link = settlementRequestLog.getLink(); + String title = settlementRequestLog.getLogTitle().getTitle(); + if (title.equals(LogTitle.SETTLEMENT_REQUEST.getTitle()) || title.equals(LogTitle.SETTLEMENT_RE_REQUEST.getTitle())) { + if (!settlementRequestRepository.existsByTravelId(settlementRequestLog.getTravel().getId())) { + link = null; + } + } + + SettlementRequestLogDto settlementRequestLogDto = SettlementRequestLogDto.of(settlementRequestLog, link); + settlementRequestLogDtos.add(settlementRequestLogDto); + } + + return settlementRequestLogDtos; + } + + @Override + public void createSettlementLogsForEndedTravels() { + List travels = travelRepository.findAllByTravelEndDate(LocalDate.now()); + for (Travel travel : travels) { + List travelMembers = travelMemberRepository.findAllByTravelId(travel.getId()); + travelMembers.forEach(travelMember -> { + User user = travelMember.getUser(); + createSettlementRequestLog(travel, user, LogTitle.PAYMENT_REQUEST); + }); + } + } + + @Override + public void createSettlementReRequestLogForNotAgreed(SettlementRequest settlementRequest) { + Travel travel = settlementRequest.getTravel(); + travelMemberSettlementHistoryRepository.findAllBySettlementRequestId(settlementRequest.getId()) + .stream() + .filter(travelMemberSettlementHistory -> !travelMemberSettlementHistory.isAgreed()) + .forEach(travelMemberSettlementHistory -> { + User user = travelMemberSettlementHistory.getTravelMember().getUser(); + createSettlementRequestLog(travel, user, LogTitle.SETTLEMENT_RE_REQUEST); + }); + } + + private void createSettlementRequestLog(Travel travel, User user, LogTitle logTitle) { + SettlementRequestLog settlementRequestLog = SettlementRequestLog.builder() + .travel(travel) + .user(user) + .logTitle(logTitle) + .logMessage(logTitle.getMessage(travel.getTravelName())) + .link(logTitle.getLinkPattern(travel.getId())) + .build(); + settlementRequestLogRepository.save(settlementRequestLog); + + // ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ „์†ก + sendNotification(settlementRequestLog); + } + + private List getSettlementRequestLogsByUserId(Long userId) { + return settlementRequestLogRepository.findAllByUserId(userId); + } + + private void sendNotification(SettlementRequestLog settlementRequestLog) { + String userId = String.valueOf(settlementRequestLog.getUser().getId()); + + // ์ˆ˜์‹ ์ž์— ์—ฐ๊ฒฐ๋œ ๋ชจ๋“  SseEmitter ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ด + Map emitters = + emitterRepository.findAllEmitterStartWithByUserId(userId); + + // eventId ์ƒ์„ฑ + String eventId = userId + "_" + System.currentTimeMillis(); + + // emitter๋ฅผ ์ˆœํ™˜ํ•˜๋ฉฐ ๊ฐ SseEmitter ๊ฐ์ฒด์— ์•Œ๋ฆผ ์ „์†ก + emitters.forEach( + (key, sseEmitter) -> { + Map eventData = new HashMap<>(); + eventData.put("title", settlementRequestLog.getLogTitle().getTitle()); // ๋กœ๊ทธ ํƒ€์ดํ‹€ (ex. ์ •์‚ฐ ์š”์ฒญ) + eventData.put("message", settlementRequestLog.getLogMessage()); // ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + eventData.put("link", settlementRequestLog.getLink()); // ์ด๋™ ๋งํฌ + emitterRepository.saveEventCache(key, eventData); + try { + sseEmitter.send(SseEmitter.event().id(eventId).name("message").data(eventData)); + } catch (IOException e) { + emitterRepository.deleteById(key); + } + } + ); + } +} + diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentCategoryClassificationService.java b/src/main/java/withbeetravel/service/payment/SharedPaymentCategoryClassificationService.java new file mode 100644 index 00000000..66d3e508 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentCategoryClassificationService.java @@ -0,0 +1,8 @@ +package withbeetravel.service.payment; + +import withbeetravel.domain.Category; + +public interface SharedPaymentCategoryClassificationService { + + public Category getCategory(String storeName); +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentCategoryClassificationServiceImpl.java b/src/main/java/withbeetravel/service/payment/SharedPaymentCategoryClassificationServiceImpl.java new file mode 100644 index 00000000..26ce1ae4 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentCategoryClassificationServiceImpl.java @@ -0,0 +1,101 @@ +package withbeetravel.service.payment; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import okhttp3.*; +import org.springframework.stereotype.Service; +import withbeetravel.config.OpenAIConfig; +import withbeetravel.domain.Category; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.TravelErrorCode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class SharedPaymentCategoryClassificationServiceImpl implements SharedPaymentCategoryClassificationService { + + // ์ƒ์„ฑํ˜• AI ๊ด€๋ จ ํ•„๋“œ + private final OpenAIConfig config; + private final ObjectMapper objectMapper; + private final OkHttpClient client = new OkHttpClient(); + + // ์ƒ์„ฑํ˜• AI ๋ฉ”์‹œ์ง€ ํด๋ž˜์Šค + @RequiredArgsConstructor + @Getter + private static class Message { + private final String role; + private final String content; + } + + @Override + public Category getCategory(String storeName) { + try { + List messages = new ArrayList<>(); + messages.add(new Message("system", + "๋‹น์‹ ์€ ์—ฌํ–‰ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ถ„๋ฅ˜ํ•˜๋Š” AI์ž…๋‹ˆ๋‹ค. " + + "์ƒํ˜ธ๋ช…์„ ๋ณด๊ณ  ๋‹ค์Œ ์นดํ…Œ๊ณ ๋ฆฌ ์ค‘ ํ•˜๋‚˜๋กœ๋งŒ ๋ถ„๋ฅ˜ํ•ด์ฃผ์„ธ์š”: " + + "ํ•ญ๊ณต, ๊ตํ†ต, ์ˆ™๋ฐ•, ์‹๋น„, ๊ด€๊ด‘, ์•กํ‹ฐ๋น„ํ‹ฐ, ์‡ผํ•‘, ๊ธฐํƒ€ " + + "์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„๋งŒ ์ •ํ™•ํžˆ ๋‹ต๋ณ€ํ•ด์ฃผ์„ธ์š”. " + + "- ํ˜ธํ…”, ๋ฆฌ์กฐํŠธ, ์—์–ด๋น„์•ค๋น„ ๋“ฑ์€ '์ˆ™๋ฐ•' " + + "- ์‹๋‹น, ์นดํŽ˜, ๋ฐ” ๋“ฑ์€ '์‹๋น„' " + + "- ๋ฒ„์Šค, ํƒ์‹œ, ์ง€ํ•˜์ฒ , ๊ธฐ์ฐจ ๋“ฑ์€ '๊ตํ†ต' " + + "- ํ•ญ๊ณต์‚ฌ, ๊ณตํ•ญ ๋“ฑ์€ 'ํ•ญ๊ณต' " + + "- ๋ฐ•๋ฌผ๊ด€, ๋ฏธ์ˆ ๊ด€, ๋žœ๋“œ๋งˆํฌ ๋“ฑ์€ '๊ด€๊ด‘' " + + "- ํ…Œ๋งˆํŒŒํฌ, ์Šคํฌ์ธ , ์ฒดํ—˜ ๋“ฑ์€ '์•กํ‹ฐ๋น„ํ‹ฐ' " + + "- ๋งˆํŠธ, ์‡ผํ•‘๋ชฐ, ์•„์šธ๋ › ๋“ฑ์€ '์‡ผํ•‘' ์œผ๋กœ ๋ถ„๋ฅ˜ํ•ด์ฃผ์„ธ์š”." + )); + + messages.add(new Message("user", String.format("์ƒํ˜ธ๋ช…: %s", storeName))); + + String requestBody = objectMapper.writeValueAsString(Map.of( + "model", config.getModel(), + "messages", messages, + "temperature", 0.3 + )); + + Request request = new Request.Builder() + .url(config.getEndpoint()) + .post(RequestBody.create( + requestBody, + MediaType.parse("application/json") + )) + .addHeader("Authorization", "Bearer " + config.getApiKey()) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new CustomException(TravelErrorCode.TRAVEL_CATEGORY_NOT_FOUND); + } + + JsonNode jsonResponse = objectMapper.readTree(response.body().string()); + String categoryName = jsonResponse + .path("choices") + .get(0) + .path("message") + .path("content") + .asText() + .trim(); + + // ๋ฐ˜ํ™˜๋œ ์นดํ…Œ๊ณ ๋ฆฌ๋ช…์„ Category enum์œผ๋กœ ๋ณ€ํ™˜ + return switch (categoryName) { + case "ํ•ญ๊ณต" -> Category.FLIGHT; + case "๊ตํ†ต" -> Category.TRANSPORTATION; + case "์ˆ™๋ฐ•" -> Category.ACCOMMODATION; + case "์‹๋น„" -> Category.FOOD; + case "๊ด€๊ด‘" -> Category.TOUR; + case "์•กํ‹ฐ๋น„ํ‹ฐ" -> Category.ACTIVITY; + case "์‡ผํ•‘" -> Category.SHOPPING; + default -> Category.ETC; + }; + } + } catch (Exception e) { + // OpenAI API ํ˜ธ์ถœ ์‹คํŒจ์‹œ ๊ธฐํƒ€๋กœ ๋ถ„๋ฅ˜ + return Category.ETC; + } + } +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentParticipantService.java b/src/main/java/withbeetravel/service/payment/SharedPaymentParticipantService.java new file mode 100644 index 00000000..3657a246 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentParticipantService.java @@ -0,0 +1,13 @@ +package withbeetravel.service.payment; + +import withbeetravel.dto.request.payment.SharedPaymentParticipateRequest; +import withbeetravel.dto.response.SuccessResponse; + +public interface SharedPaymentParticipantService { + + public void updateParticipantMembers( + Long travelId, + Long sharedPaymentId, + SharedPaymentParticipateRequest sharedPaymentParticipateRequest + ); +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentParticipantServiceImpl.java b/src/main/java/withbeetravel/service/payment/SharedPaymentParticipantServiceImpl.java new file mode 100644 index 00000000..4a857cff --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentParticipantServiceImpl.java @@ -0,0 +1,145 @@ +package withbeetravel.service.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.PaymentParticipatedMember; +import withbeetravel.domain.SharedPayment; +import withbeetravel.domain.TravelMember; +import withbeetravel.dto.request.payment.SharedPaymentParticipateRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.PaymentErrorCode; +import withbeetravel.repository.PaymentParticipatedMemberRepository; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelMemberRepository; + +import java.util.Collections; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SharedPaymentParticipantServiceImpl implements SharedPaymentParticipantService { + + private final TravelMemberRepository travelMemberRepository; + private final SharedPaymentRepository sharedPaymentRepository; + private final PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + + @Override + @Transactional + public void updateParticipantMembers( + Long travelId, + Long sharedPaymentId, + SharedPaymentParticipateRequest sharedPaymentParticipateRequest + ) { + // ์—ฌํ–‰ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ + List allByTravelId = + travelMemberRepository.findAllByTravelId(travelId); + + // ์ˆ˜์ •ํ•  ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ + List newParticipateMembersId = sharedPaymentParticipateRequest.getTravelMembersId(); + + // ์ž…๋ ฅ์œผ๋กœ ๋“ค์–ด์˜จ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ ์ค‘์— ์—ฌํ–‰ ๋ฉค๋ฒ„๊ฐ€ ์•„๋‹Œ travelMemberId๊ฐ€ ์žˆ๋‚˜ ๊ฒ€์‚ฌ + isAllMemberIdInTravelMemberId(allByTravelId, newParticipateMembersId); + + // ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ๊ฐ€์ ธ์˜ค๊ธฐ + SharedPayment sharedPayment = sharedPaymentRepository.findById(sharedPaymentId) + .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND)); + + // ๊ธฐ์กด ์ •์‚ฐ ์ธ์› ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + List allBySharedPaymentId = + paymentParticipatedMemberRepository.findAllBySharedPaymentId(sharedPaymentId); + + // ์ง€์šธ ๋ฉค๋ฒ„ ์ง€์šฐ๊ณ , ์ถ”๊ฐ€ํ•  ๋ฉค๋ฒ„ ์ถ”๊ฐ€ + addOrRemoveParticipateMembers(sharedPayment, allByTravelId, newParticipateMembersId, allBySharedPaymentId); + + // ์ •์‚ฐ ์ธ์› ์ˆ˜์ • + sharedPayment.updateParticipantCount(newParticipateMembersId.size()); + } + + void isAllMemberIdInTravelMemberId(List allByTravelId, List newParticipateMembersId) { + + for (int i = 0; i < newParticipateMembersId.size(); i++) { + + boolean flag = false; + + for (int j = 0; j < allByTravelId.size(); j++) { + + if(newParticipateMembersId.get(i).equals(allByTravelId.get(j).getId())) { + flag = true; + break; + } + } + + // ์—ฌํ–‰ ๋ฉค๋ฒ„์— ํฌํ•จ๋˜์ง€ ์•Š์€ travelMemberId ๋ฐœ๊ฒฌ + if(!flag) throw new CustomException(PaymentErrorCode.NON_TRAVEL_MEMBER_INCLUDED); + } + + } + + void addOrRemoveParticipateMembers( + SharedPayment sharedPayment, + List allByTravelId, + List newParticipateMembersId, + List allBySharedPaymentId + ) { + // ๋‘ ๋ฆฌ์ŠคํŠธ ์ •๋ ฌ + Collections.sort(newParticipateMembersId); + Collections.sort(allBySharedPaymentId, + (o1, o2) -> o1.getTravelMember().getId() < o2.getTravelMember().getId() ? 0 : 1); + + // ํˆฌ ํฌ์ธํ„ฐ + int p1 = 0, p2 = 0; + + while(p1 < newParticipateMembersId.size() && p2 < allBySharedPaymentId.size()) { + + // ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฉค๋ฒ„์ธ ๊ฒฝ์šฐ + if(newParticipateMembersId.get(p1).equals(allBySharedPaymentId.get(p2).getTravelMember().getId())) { + p1++; + p2++; + } + // ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋Š” ๋ฉค๋ฒ„๋ฅผ ์ฐพ์€ ๊ฒฝ์šฐ + else if(newParticipateMembersId.get(p1) < allBySharedPaymentId.get(p2).getTravelMember().getId()) { + PaymentParticipatedMember newPaymentParticipatedMember = + PaymentParticipatedMember.builder() + .sharedPayment(sharedPayment) + .travelMember(findTravelMemberByTravelMemberId(allByTravelId, newParticipateMembersId.get(p1))) + .build(); + paymentParticipatedMemberRepository.save(newPaymentParticipatedMember); + p1++; + } + // ์‚ญ์ œํ•ด์•ผ ํ•˜๋Š” ๋ฉค๋ฒ„๋ฅผ ์ฐพ์€ ๊ฒฝ์šฐ + else { + paymentParticipatedMemberRepository.delete(allBySharedPaymentId.get(p2)); + p2++; + } + } + + while(p1 < newParticipateMembersId.size()) { + PaymentParticipatedMember newPaymentParticipatedMember = + PaymentParticipatedMember.builder() + .sharedPayment(sharedPayment) + .travelMember(findTravelMemberByTravelMemberId(allByTravelId, newParticipateMembersId.get(p1))) + .build(); + paymentParticipatedMemberRepository.save(newPaymentParticipatedMember); + p1++; + } + + while(p2 < allBySharedPaymentId.size()) { + paymentParticipatedMemberRepository.delete(allBySharedPaymentId.get(p2)); + p2++; + } + } + + // ์ „์ฒด ์—ฌํ–‰ ๋ฉค๋ฒ„ ์ค‘, travelMemberId์— ํ•ด๋‹นํ•˜๋Š” TravelMember ๋ฐ˜ํ™˜ + TravelMember findTravelMemberByTravelMemberId(List allByTravelId, Long travelMemberId) { + + for (int i = 0; i < allByTravelId.size(); i++) { + if(allByTravelId.get(i).getId().equals(travelMemberId)) return allByTravelId.get(i); + } + + // travelMemberId์— ํ•ด๋‹นํ•˜๋Š” ์—ฌํ–‰ ๋ฉค๋ฒ„๊ฐ€ ์—†๋‹ค๋ฉด + throw new CustomException(PaymentErrorCode.NON_TRAVEL_MEMBER_INCLUDED); + } +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentRecordService.java b/src/main/java/withbeetravel/service/payment/SharedPaymentRecordService.java new file mode 100644 index 00000000..aecf4d42 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentRecordService.java @@ -0,0 +1,20 @@ +package withbeetravel.service.payment; + +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.dto.response.payment.SharedPaymentRecordResponse; +import withbeetravel.dto.response.SuccessResponse; + +public interface SharedPaymentRecordService { + + void addAndUpdatePaymentRecord( + Long travelId, + Long sharedPaymentId, + MultipartFile image, + String comment, + boolean isMainImage + ); + + SharedPaymentRecordResponse getSharedPaymentRecord( + Long sharedPaymentId + ); +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentRecordServiceImpl.java b/src/main/java/withbeetravel/service/payment/SharedPaymentRecordServiceImpl.java new file mode 100644 index 00000000..5ae41ca9 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentRecordServiceImpl.java @@ -0,0 +1,116 @@ +package withbeetravel.service.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.domain.SharedPayment; +import withbeetravel.domain.Travel; +import withbeetravel.dto.response.payment.SharedPaymentRecordResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.PaymentErrorCode; +import withbeetravel.exception.error.TravelErrorCode; +import withbeetravel.exception.error.ValidationErrorCode; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelRepository; +import withbeetravel.service.global.S3Uploader; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +public class SharedPaymentRecordServiceImpl implements SharedPaymentRecordService { + + private final S3Uploader s3Uploader; + + private final TravelRepository travelRepository; + private final SharedPaymentRepository sharedPaymentRepository; + + // S3์— ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ๊ฒฝ๋กœ + private static final String SHARED_PAYMENT_IMAGE_DIR = "travels/"; + + @Override + @Transactional + public void addAndUpdatePaymentRecord( + Long travelId, + Long sharedPaymentId, + MultipartFile image, + String comment, + boolean isMainImage) { + + // SharedPayment Entity ๊ฐ€์ ธ์˜ค๊ธฐ + SharedPayment sharedPayment = sharedPaymentRepository.findById(sharedPaymentId) + .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND)); + + // ์—ฌํ–‰ ์ •๋ณด ์ฐพ์•„์˜ค๊ธฐ + Travel travel = travelRepository.findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + + // ์ด๋ฏธ์ง€ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ + if(image != null && image.getSize() != 0) { + String newImageUrl = uploadImage(travelId, sharedPayment, image); + sharedPayment.updatePaymentImage(newImageUrl); + // ๋ฉ”์ธ ์ด๋ฏธ์ง€๋กœ ์„ค์ •ํ–ˆ๋‹ค๋ฉด, ์—ฌํ–‰ ๋ฉ”์ธ ์‚ฌ์ง„ ๋ฐ”๊ฟ”์ฃผ๊ธฐ + if(isMainImage) { + // ์—ฌํ–‰ ์ด๋ฏธ์ง€ ์ˆ˜์ • + travel.updateMainImage(newImageUrl); + } + } + // ๊ธฐ์กด ์ด๋ฏธ์ง€๋ฅผ ๋ฉ”์ธ ์ด๋ฏธ์ง€๋กœ ์„ค์ •ํ•œ ๊ฒฝ์šฐ + else if(isMainImage && sharedPayment.getPaymentImage() != null) { + // ์—ฌํ–‰ ์ด๋ฏธ์ง€ ์ˆ˜์ • + travel.updateMainImage(sharedPayment.getPaymentImage()); + } + + // comment ์ •๋ณด ์—”ํ‹ฐํ‹ฐ์—์„œ ๋ณ€๊ฒฝ + sharedPayment.updatePaymentCommnet(comment); + } + + @Override + @Transactional(readOnly = true) + public SharedPaymentRecordResponse getSharedPaymentRecord(Long sharedPaymentId) { + + // SharedPayment ์—”ํ‹ฐํ‹ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + SharedPayment sharedPayment = sharedPaymentRepository.findById(sharedPaymentId) + .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND)); + + // paymentImage๊ฐ€ ๋ฉ”์ธ ์ด๋ฏธ์ง€์ธ์ง€ ์—ฌ๋ถ€ + boolean isMainImage = sharedPayment.getPaymentImage() != null && + sharedPayment.getPaymentImage().equals(sharedPayment.getTravel().getMainImage()); + + // Response Dto์— ๋‹ด๊ธฐ + SharedPaymentRecordResponse responseDto = SharedPaymentRecordResponse.from(sharedPayment, isMainImage); + + return responseDto; + } + + // ์ด๋ฏธ์ง€ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ + private String uploadImage(Long travelId, SharedPayment sharedPayment, MultipartFile image) { + + // ์›๋ž˜ ์ด๋ฏธ์ง€ + String paymentImage = sharedPayment.getPaymentImage(); + + // ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•œ ์ด๋ฏธ์ง€ + String newImage = null; + + // image๊ฐ€ ์ƒˆ๋กœ ๋“ค์–ด์™”๋‹ค๋ฉด S3์— ์ €์žฅ + if(!image.isEmpty()) { + + // ์ด๋ฏธ์ง€ ์ €์žฅํ•  S3 ๋””๋ ‰ํ† ๋ฆฌ ์ •๋ณด + String dirName = SHARED_PAYMENT_IMAGE_DIR + travelId; + + try { + if(paymentImage != null) { // ํ•ด๋‹น ๊ณต๋™๊ฒฐ์ œ ๋‚ด์—ญ์— ์ด๋ฏธ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์—…๋ฐ์ดํŠธ + newImage = s3Uploader.update(image, paymentImage, dirName); + } else { // ์—†๋‹ค๋ฉด ์—…๋กœ๋“œ + newImage = s3Uploader.upload(image, dirName); + } + } catch (IOException e) { // ์ด๋ฏธ์ง€ ์ €์žฅ์— ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ + throw new CustomException(ValidationErrorCode.IMAGE_PROCESSING_FAILED); + } + } + + // ์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€ ์ •๋ณด ๋ฐ˜ํ™˜ + return newImage; + } +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentRegisterService.java b/src/main/java/withbeetravel/service/payment/SharedPaymentRegisterService.java new file mode 100644 index 00000000..17bc24cf --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentRegisterService.java @@ -0,0 +1,57 @@ +package withbeetravel.service.payment; + +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.domain.History; +import withbeetravel.domain.Travel; +import withbeetravel.domain.TravelMember; +import withbeetravel.dto.request.payment.SharedPaymentWibeeCardRegisterRequest; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.payment.CurrencyUnitResponse; + +public interface SharedPaymentRegisterService { + + void addManualSharedPayment( + Long userId, + Long travelId, + String paymentDate, + String storeName, + int paymentAmount, + Double foreignPaymentAmount, + String currencyUnit, + Double exchangeRate, + MultipartFile paymentImage, + String paymentComment, + boolean isMainImage + ); + + void updateManualSharedPayment( + Long userId, + Long travelId, + Long sharedPaymentId, + String paymentDate, + String storeName, + int paymentAmount, + Double foreignPaymentAmount, + String currencyUnit, + Double exchangeRate, + MultipartFile paymentImage, + String paymentComment, + boolean isMainImage + ); + + void addWibeeCardSharedPayment( + Long userId, + Long travelId, + SharedPaymentWibeeCardRegisterRequest sharedPaymentWibeeCardRegisterRequest + ); + + CurrencyUnitResponse getCurrencyUnitOptions( + Long travelId + ); + + void saveWibeeCardSharedPayment( + TravelMember travelMember, + Travel travel, + History history + ); +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentRegisterServiceImpl.java b/src/main/java/withbeetravel/service/payment/SharedPaymentRegisterServiceImpl.java new file mode 100644 index 00000000..16117663 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentRegisterServiceImpl.java @@ -0,0 +1,411 @@ +package withbeetravel.service.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.parameters.P; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.domain.*; +import withbeetravel.dto.request.payment.SharedPaymentWibeeCardRegisterRequest; +import withbeetravel.dto.response.payment.CurrencyUnitResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.*; +import withbeetravel.repository.*; +import withbeetravel.service.global.S3Uploader; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class SharedPaymentRegisterServiceImpl implements SharedPaymentRegisterService{ + + private final TravelRepository travelRepository; + private final TravelMemberRepository travelMemberRepository; + private final SharedPaymentRepository sharedPaymentRepository; + private final PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + private final UserRepository userRepository; + private final HistoryRepository historyRepository; + private final TravelCountryRepository travelCountryRepository; + + private final SharedPaymentCategoryClassificationService sharedPaymentCategoryClassificationService; + + private final S3Uploader s3Uploader; + + // S3์— ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ๊ฒฝ๋กœ + private static final String SHARED_PAYMENT_IMAGE_DIR = "travels/"; + + @Override + @Transactional + public void addManualSharedPayment( + Long userId, + Long travelId, + String paymentDate, + String storeName, + int paymentAmount, + Double foreignPaymentAmount, + String currencyUnit, + Double exchangeRate, + MultipartFile paymentImage, + String paymentComment, + boolean isMainImage + ) { + + // travelId์— ๋”ฐ๋ฅธ ์—ฌํ–‰ ๊ฐ€์ ธ์˜ค๊ธฐ + Travel travel = getTravel(travelId); + + // userId์™€ travelId์— ๋”ฐ๋ฅธ ์—ฌํ–‰ ๋ฉค๋ฒ„ ๊ฐ€์ ธ์˜ค๊ธฐ + TravelMember travelMember = getTravelMember(userId, travelId); + + // ์™ธํ™” ๊ธˆ์•ก๊ณผ ํ™˜์œจ ์ •๋ณด๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ๋“ค์–ด์™”๋‹ค๋ฉด, ๋‘ ๊ฐ’์ด ๋ชจ๋‘ ๋“ค์–ด์™”๋Š”์ง€ ํ™•์ธ + validatePaymentAmount(foreignPaymentAmount, exchangeRate); + + // ์ด๋ฏธ์ง€ ๊ฐ’์ด ๋“ค์–ด์™”์„ ๊ฒฝ์šฐ S3์— ์—…๋กœ๋“œ + String imageUrl = null; + if(paymentImage != null && paymentImage.getSize() != 0) { + try { + imageUrl = s3Uploader.upload(paymentImage, SHARED_PAYMENT_IMAGE_DIR + travelId); + } catch (IOException e) { + throw new CustomException(ValidationErrorCode.IMAGE_PROCESSING_FAILED); + } + // ๋ฉ”์ธ ์ด๋ฏธ์ง€๋กœ ์„ค์ •ํ–ˆ๋‹ค๋ฉด, ์—ฌํ–‰ ๋ฉ”์ธ ์‚ฌ์ง„ ๋ฐ”๊ฟ”์ฃผ๊ธฐ + if(isMainImage) setTravelMainImage(travel, imageUrl); + } + + + // ์—ฌํ–‰ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ + List members = getTravelMembers(travelId); + + // ์ƒˆ๋กœ์šด ๊ฒฐ์ œ ๋‚ด์—ญ ์ถ”๊ฐ€ + SharedPayment sharedPayment = SharedPayment.builder() + .addedByMember(travelMember) + .travel(travel) + .currencyUnit(CurrencyUnit.from(currencyUnit)) + .paymentAmount(paymentAmount) + .foreignPaymentAmount(foreignPaymentAmount) + .exchangeRate(exchangeRate) + .paymentComment(paymentComment) + .paymentImage(imageUrl) + .isManuallyAdded(true) + .participantCount(members.size()) + .category(getCategory(storeName)) + .storeName(storeName) + .paymentDate(dateTimeFormatter(paymentDate)) + .build(); + sharedPaymentRepository.save(sharedPayment); + + // ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ฐธ์—ฌ ๋ฉค๋ฒ„ ์ˆ˜์ • + setParticipatedMembers(sharedPayment, members); + } + + @Override + @Transactional + public void updateManualSharedPayment( + Long userId, + Long travelId, + Long sharedPaymentId, + String paymentDate, + String storeName, + int paymentAmount, + Double foreignPaymentAmount, + String currencyUnit, + Double exchangeRate, + MultipartFile paymentImage, + String paymentComment, + boolean isMainImage + ) { + + // ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + SharedPayment sharedPayment = getSharedPayment(sharedPaymentId); + + // ์—ฌํ–‰ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + Travel travel = getTravel(travelId); + + // ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์ธ์ง€ ํ™•์ธ + validateUpdateSharedPayment(userId, travelId, sharedPayment); + + // ์™ธํ™” ๊ธˆ์•ก๊ณผ ํ™˜์œจ ์ •๋ณด๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ๋“ค์–ด์™”๋‹ค๋ฉด, ๋‘ ๊ฐ’์ด ๋ชจ๋‘ ๋“ค์–ด์™”๋Š”์ง€ ํ™•์ธ + validatePaymentAmount(foreignPaymentAmount, exchangeRate); + + // ์ด๋ฏธ์ง€ ๊ฐ’์ด ๋“ค์–ด์™”์„ ๊ฒฝ์šฐ S3์— ์—…๋กœ๋“œ + String imageUrl; + try { + imageUrl = s3Uploader.update(paymentImage, sharedPayment.getPaymentImage(), SHARED_PAYMENT_IMAGE_DIR + travelId); + } catch (IOException e) { + throw new CustomException(ValidationErrorCode.IMAGE_PROCESSING_FAILED); + } + + // ๋ฉ”์ธ ์ด๋ฏธ์ง€๋กœ ์„ค์ •ํ–ˆ๋‹ค๋ฉด, ์—ฌํ–‰ ๋ฉ”์ธ ์‚ฌ์ง„ ๋ฐ”๊ฟ”์ฃผ๊ธฐ + if(isMainImage) setTravelMainImage(travel, imageUrl); + + // ๊ณต๋™ ๋‚ด์—ญ ์ˆ˜์ • + sharedPayment.updateManuallyPayment( + CurrencyUnit.from(currencyUnit), + paymentAmount, + foreignPaymentAmount, + exchangeRate, + paymentComment, + imageUrl, + getCategory(storeName), + storeName, + dateTimeFormatter(paymentDate) + ); + } + + @Override + @Transactional + public void addWibeeCardSharedPayment( + Long userId, + Long travelId, + SharedPaymentWibeeCardRegisterRequest sharedPaymentWibeeCardRegisterRequest + ) { + + // ํšŒ์› ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + User user = getUser(userId); + + // ์œ„๋น„ ํŠธ๋ž˜๋ธ” ์นด๋“œ๊ฐ€ ์—ฐ๋™๋˜์–ด ์žˆ๋Š” ๊ณ„์ขŒ + Account account = getConnectedWibeeCardAccount(user); + + // ์—ฌํ–‰ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + Travel travel = getTravel(travelId); + + // ์—ฌํ–‰ ๋ฉค๋ฒ„ ๊ฐ€์ ธ์˜ค๊ธฐ + TravelMember travelMember = getTravelMember(userId, travelId); + + // ์ž…๋ ฅ์œผ๋กœ ๋“ค์–ด์˜จ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์„ ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์œผ๋กœ ์ €์žฅ + registerWibeeCardSharedPayment( + sharedPaymentWibeeCardRegisterRequest.getHistoryId(), + travel, + travelMember, + account + ); + } + + @Override + @Transactional(readOnly = true) + public CurrencyUnitResponse getCurrencyUnitOptions(Long travelId) { + + Travel travel = getTravel(travelId); + + // ๋ฐฉ๋ฌธ ์˜ˆ์ • ๋‚˜๋ผ์˜ ํ†ตํ™” ์ฝ”๋“œ + Set visitedCountryCurrencyUnit = new HashSet<>(); + + // ํ•ด์™ธ ์—ฌํ–‰์ด๋ฉด ๋ฐฉ๋ฌธ ๋‚˜๋ผ์˜ ํ†ตํ™” ์ฝ”๋“œ๋“ค ๋„ฃ๊ธฐ + if(!travel.isDomesticTravel()) { + List travelCountries = getTravelCountries(travelId); + for(TravelCountry country: travelCountries) { + if(!country.getCountry().getCurrencyCode().equals("KRW")) + visitedCountryCurrencyUnit.add(country.getCountry().getCurrencyCode()); + } + } + + // visitedCountryCurrencyUnit์— ์—†๋Š” ํ†ตํ™” ์ฝ”๋“œ ๋ฆฌ์ŠคํŠธ + List doesntVisitedCountryCurrencyUnit = new ArrayList<>(); + for(CurrencyUnit currencyUnit : CurrencyUnit.values()) { + if(!visitedCountryCurrencyUnit.contains(currencyUnit.name()) && !currencyUnit.name().equals("KRW")) + doesntVisitedCountryCurrencyUnit.add(currencyUnit.name()); + } + + // doesntVisitedCountryCurrencyUnit ์‚ฌ์ „์ˆœ ์ •๋ ฌ + Collections.sort(doesntVisitedCountryCurrencyUnit); + + // visitedCountryCurrencyUnit๋ฅผ currencyUnitOptions์— ๋‹ด์•„ ์‚ฌ์ „ ์ˆœ ์ •๋ ฌ + List currencyUnitOptions = new ArrayList<>(visitedCountryCurrencyUnit); + Collections.sort(currencyUnitOptions); + + // ret ๋’ค์— doesntVisitedCountryCurrencyUnit ์ถ”๊ฐ€ + currencyUnitOptions.addAll(doesntVisitedCountryCurrencyUnit); + + // ์›ํ™”๋Š” ํ•ญ์ƒ ๋งจ ์•ž + currencyUnitOptions.add(0, "KRW"); + + return CurrencyUnitResponse.builder() + .currencyUnitOptions(currencyUnitOptions) + .build(); + } + + Travel getTravel(Long travelId) { + return travelRepository.findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + } + + TravelMember getTravelMember(Long userId, Long travelId) { + return travelMemberRepository.findByTravelIdAndUserId(travelId, userId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN)); + } + + List getTravelMembers(Long travelId) { + return travelMemberRepository.findAllByTravelId(travelId); + } + + SharedPayment getSharedPayment(Long sharedPaymentId) { + return sharedPaymentRepository.findById(sharedPaymentId) + .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND)); + } + + Category getCategory(String storeName) { + return sharedPaymentCategoryClassificationService.getCategory(storeName); + } + + User getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.AUTHENTICATION_FAILED)); + } + + Account getConnectedWibeeCardAccount(User user) { + + // ์œ„๋น„ ์นด๋“œ๋ฅผ ๋ฐœ๊ธ‰ ๋ฐ›์ง€ ์•Š์€ ํšŒ์›์ธ ๊ฒฝ์šฐ + if(user.getWibeeCardAccount() == null) + throw new CustomException(BankingErrorCode.WIBEE_CARD_NOT_ISSUED); + + return user.getWibeeCardAccount(); + } + + History getHistory(Long historyId) { + return historyRepository.findById(historyId) + .orElseThrow(() -> new CustomException(BankingErrorCode.HISTORY_NOT_FOUND)); + } + + List getTravelCountries(Long travelId) { + return travelCountryRepository.findByTravelId(travelId); + } + + void validatePaymentAmount(Double foreignPaymentAmount, Double exchangeRate) { + + // ๋‘˜ ๋‹ค null ๊ฐ’์ด๊ฑฐ๋‚˜, null ๊ฐ’์ด ์•„๋‹ˆ๊ฑฐ๋‚˜ + if((foreignPaymentAmount == null && exchangeRate == null) || + (foreignPaymentAmount != null && exchangeRate != null)) return; + + throw new CustomException(ValidationErrorCode.MISSING_REQUIRED_FIELDS); + } + + void validateUpdateSharedPayment(Long userId, Long travelId, SharedPayment sharedPayment) { + + // ์ง์ ‘ ์ถ”๊ฐ€ ๊ฒฐ์ œ ๋‚ด์—ญ์ด ์•„๋‹Œ ๊ฒฝ์šฐ, ์ˆ˜์ • ๋ถˆ๊ฐ€ + if(!sharedPayment.isManuallyAdded()) + throw new CustomException(PaymentErrorCode.NO_PERMISSION_TO_MODIFY_SHARED_PAYMENT); + + // ๋กœ๊ทธ์ธ๋œ ํšŒ์›์ด ํ•ด๋‹น ๊ฒฐ์ œ ๋‚ด์—ญ์„ ์ถ”๊ฐ€ํ•œ ๋ฉค๋ฒ„๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ, ์ˆ˜์ • ๋ถˆ๊ฐ€ + TravelMember travelMember = getTravelMember(userId, travelId); + if(!sharedPayment.getAddedByMember().equals(travelMember)) + throw new CustomException(PaymentErrorCode.NO_PERMISSION_TO_MODIFY_SHARED_PAYMENT); + } + + void validateHistoryToAccount(History history, Account account) { + + if(history.getAccount() != account) + throw new CustomException(BankingErrorCode.HISTORY_ACCESS_FORBIDDEN); + } + + void validateisWibeeCardUsedHistory(History history) { + + if(!history.isWibeeCard()) { + throw new CustomException(BankingErrorCode.HISTORY_ACCESS_FORBIDDEN); + } + } + + void validateHistoryDate(Travel travel, History history) { + + // ์—ฌํ–‰์ผ ์ด์ „์ด๋‚˜ ์ดํ›„์— ๋ฐœ์ƒํ•œ ๊ฒฐ์ œ ๋‚ด์—ญ์ด๋ฉด ๊ฒ€์‚ฌ ํ†ต๊ณผ + if (travel.getTravelStartDate().isAfter(history.getDate().toLocalDate()) + || travel.getTravelEndDate().isBefore(history.getDate().toLocalDate())) + return; + + throw new CustomException(ValidationErrorCode.DATE_RANGE_ERROR); + } + + void validateisAddedHistory(History history) { + if(history.isAddedSharedPayment()) + throw new CustomException(BankingErrorCode.PAYMENT_ALREADY_EXISTS); + } + + LocalDateTime dateTimeFormatter(String date) { + + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return LocalDateTime.parse(date, formatter); + } catch (DateTimeParseException e) { + throw new CustomException(ValidationErrorCode.INVALID_DATE_FORMAT); + } + + } + + void setParticipatedMembers(SharedPayment sharedPayment, List members) { + for (TravelMember member : members) { + paymentParticipatedMemberRepository.save( + PaymentParticipatedMember.builder() + .travelMember(member) + .sharedPayment(sharedPayment) + .build() + ); + } + } + + void setTravelMainImage(Travel travel, String imageUrl) { + + // ์ด๋ฏธ์ง€๊ฐ€ ์—†๋Š”๋ฐ ๋ฉ”์ธ ์ด๋ฏธ์ง€๋กœ ์„ค์ •ํ•œ ๊ฒฝ์šฐ + if(imageUrl == null) + throw new CustomException(ValidationErrorCode.MISSING_REQUIRED_FIELDS); + + travel.updateMainImage(imageUrl); + } + + @Override + public void saveWibeeCardSharedPayment( + TravelMember travelMember, + Travel travel, + History history + ) { + + SharedPayment sharedPayment = SharedPayment.builder() + .addedByMember(travelMember) + .travel(travel) + .currencyUnit(CurrencyUnit.KRW) + .paymentAmount(history.getPayAM()) + .isManuallyAdded(false) + .participantCount(travel.getTravelMembers().size()) + .category(getCategory(history.getRqspeNm())) + .storeName(history.getRqspeNm()) + .paymentDate(history.getDate()) + .build(); + + + sharedPaymentRepository.save(sharedPayment); + + // ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ์ฐธ์—ฌ ์ธ์› ์„ค์ • + setParticipatedMembers(sharedPayment, travel.getTravelMembers()); + } + + void registerWibeeCardSharedPayment( + List historyId, + Travel travel, + TravelMember travelMember, + Account account + ) { + for (Long id : historyId) { + // ๊ฑฐ๋ž˜ ๋‚ด์—ญ ๊ฐ€์ ธ์˜ค๊ธฐ + History history = getHistory(id); + + // ์œ„๋น„ ์นด๋“œ์— ์—ฐ๊ฒฐ๋œ ๊ณ„์ขŒ์˜ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์ด ๋งž๋Š”์ง€ ํ™•์ธ + validateHistoryToAccount(history, account); + + // ์œ„๋น„ ์นด๋“œ ๊ฒฐ์ œ ๋‚ด์—ญ์ด ๋งž๋Š”์ง€ ํ™•์ธ + validateisWibeeCardUsedHistory(history); + + // ํ•ด๋‹น ๊ฑฐ๋ž˜ ๋‚ด์—ญ์ด ์—ฌํ–‰ ๊ธฐ๊ฐ„ ์ „์ด๋‚˜ ํ›„์— ๋ฐœ์ƒํ•œ ๊ฑฐ๋ž˜๋‚ด์—ญ์ด ๋งž๋Š”์ง€ + validateHistoryDate(travel, history); + + // ์ด๋ฏธ ๊ฐ€์ ธ์˜จ ๊ฑฐ๋ž˜ ๋‚ด์—ญ์ธ์ง€ ํ™•์ธ + validateisAddedHistory(history); + + // ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ์— ์ถ”๊ฐ€ + saveWibeeCardSharedPayment(travelMember, travel, history); + + // ์ถ”๊ฐ€ ํ›„ ์ƒํƒœ ๋ณ€๊ฒฝ + history.addedSharedPayment(); + } + } +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentService.java b/src/main/java/withbeetravel/service/payment/SharedPaymentService.java new file mode 100644 index 00000000..60440f45 --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentService.java @@ -0,0 +1,18 @@ +package withbeetravel.service.payment; + +import org.springframework.data.domain.Page; +import withbeetravel.domain.SharedPayment; +import withbeetravel.dto.request.payment.SharedPaymentSearchRequest; +import withbeetravel.dto.response.payment.SharedPaymentParticipatingMemberResponse; +import withbeetravel.dto.response.payment.SharedPaymentResponse; +import withbeetravel.dto.response.SuccessResponse; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +public interface SharedPaymentService { + Page getSharedPayments(Long travelId, SharedPaymentSearchRequest condition); + + Map> getParticipatingMembersMap(Page payments); +} diff --git a/src/main/java/withbeetravel/service/payment/SharedPaymentServiceImpl.java b/src/main/java/withbeetravel/service/payment/SharedPaymentServiceImpl.java new file mode 100644 index 00000000..9628e94f --- /dev/null +++ b/src/main/java/withbeetravel/service/payment/SharedPaymentServiceImpl.java @@ -0,0 +1,71 @@ +package withbeetravel.service.payment; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.Category; +import withbeetravel.domain.SharedPayment; +import withbeetravel.dto.request.payment.SharedPaymentSearchRequest; +import withbeetravel.dto.response.payment.SharedPaymentParticipatingMemberResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.PaymentErrorCode; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelMemberRepository; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class SharedPaymentServiceImpl implements SharedPaymentService { + + private final SharedPaymentRepository sharedPaymentRepository; + private final TravelMemberRepository travelMemberRepository; + + @Override + @Transactional(readOnly = true) + public Page getSharedPayments(Long travelId, SharedPaymentSearchRequest condition) { + Category category = null; + try { + if (condition.getCategory() != null && !condition.getCategory().isBlank()) { + category = Category.fromString(condition.getCategory()); + } + } catch (Error e) { + throw new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND); + } + + Page payments = sharedPaymentRepository.findAllByTravelIdAndMemberIdAndDateRange( + travelId, + condition.getMemberId(), + condition.getStartDate(), + condition.getEndDate(), + category, + PageRequest.of(condition.getPage(), 10, + Sort.by(Sort.Direction.DESC, condition.getSortBy().equals("amount") ? "paymentAmount" : "paymentDate")) + ); + + if (payments.isEmpty()) { + throw new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND); + } + + return payments; + } + + @Override + public Map> getParticipatingMembersMap(Page payments) { + return payments.getContent().stream() + .collect(Collectors.toMap( + SharedPayment::getId, + payment -> payment.getPaymentParticipatedMembers().stream() + .map(ppm -> SharedPaymentParticipatingMemberResponse.builder() + .id(ppm.getTravelMember().getId()) + .profileImage(ppm.getTravelMember().getUser().getProfileImage()) + .build()) + .toList() + )); + } +} diff --git a/src/main/java/withbeetravel/service/settlement/SettlementPendingService.java b/src/main/java/withbeetravel/service/settlement/SettlementPendingService.java new file mode 100644 index 00000000..2b1067cc --- /dev/null +++ b/src/main/java/withbeetravel/service/settlement/SettlementPendingService.java @@ -0,0 +1,17 @@ +package withbeetravel.service.settlement; + +import withbeetravel.domain.SettlementRequest; +import withbeetravel.domain.SettlementRequestLog; +import withbeetravel.domain.TravelMember; +import withbeetravel.domain.TravelMemberSettlementHistory; + +import java.util.List; + +public interface SettlementPendingService { + + void handlePendingSettlementRequest(List settlementRequestLogs, + List insufficientBalanceMembers, + SettlementRequest settlementRequest, + int updatedCount, + TravelMemberSettlementHistory travelMemberSettlementHistory); +} diff --git a/src/main/java/withbeetravel/service/settlement/SettlementPendingServiceImpl.java b/src/main/java/withbeetravel/service/settlement/SettlementPendingServiceImpl.java new file mode 100644 index 00000000..ae9479d0 --- /dev/null +++ b/src/main/java/withbeetravel/service/settlement/SettlementPendingServiceImpl.java @@ -0,0 +1,91 @@ +package withbeetravel.service.settlement; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.domain.*; +import withbeetravel.repository.SettlementRequestLogRepository; +import withbeetravel.repository.SettlementRequestRepository; +import withbeetravel.repository.TravelMemberSettlementHistoryRepository; +import withbeetravel.repository.notification.EmitterRepository; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class SettlementPendingServiceImpl implements SettlementPendingService { + + private final SettlementRequestLogRepository settlementRequestLogRepository; + private final TravelMemberSettlementHistoryRepository travelMemberSettlementHistoryRepository; + private final SettlementRequestRepository settlementRequestRepository; + private final EmitterRepository emitterRepository; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void handlePendingSettlementRequest(List settlementRequestLogs, + List insufficientBalanceMembers, + SettlementRequest settlementRequest, + int updatedCount, + TravelMemberSettlementHistory travelMemberSettlementHistory) { + // ์ •์‚ฐ ๋ณด๋ฅ˜ ๋กœ๊ทธ ์ €์žฅ + settlementRequestLogRepository.saveAll(settlementRequestLogs); + + // ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ „์†ก + sendNotification(settlementRequestLogs); + + // ์ž”์•ก ๋ถ€์กฑ ๋ฉค๋ฒ„์˜ ์ •์‚ฐ ๋™์˜๋ฅผ true -> false๋กœ ๋ณ€๊ฒฝ + changeIsAgreedToFalse(insufficientBalanceMembers, settlementRequest); + + // ์ž์‹ ์˜ isAgreed๋Š” true๋กœ ๋ณ€๊ฒฝ (์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ˆ˜๋™์œผ๋กœ save ํ•„์š”) + travelMemberSettlementHistory.updateIsAgreed(true); + travelMemberSettlementHistoryRepository.save(travelMemberSettlementHistory); + + // ์ •์‚ฐ ๋ฏธ๋™์˜ ์ธ์›์ˆ˜ ๋ณ€๊ฒฝ (์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ˆ˜๋™์œผ๋กœ save ํ•„์š”) + settlementRequest.updateDisagreeCount(insufficientBalanceMembers.size() - 1); + settlementRequestRepository.save(settlementRequest); + } + + private void changeIsAgreedToFalse(List insufficientBalanceMembers, SettlementRequest settlementRequest) { + for (TravelMember insufficientBalanceMember : insufficientBalanceMembers) { + TravelMemberSettlementHistory insufficientTravelMemberSettlementHistory = + travelMemberSettlementHistoryRepository + .findTravelMemberSettlementHistoryBySettlementRequestIdAndTravelMemberId( + settlementRequest.getId(), insufficientBalanceMember.getId()); + insufficientTravelMemberSettlementHistory.updateIsAgreed(false); + } + } + + private void sendNotification(List settlementRequestLogs) { + for (SettlementRequestLog settlementRequestLog : settlementRequestLogs) { + String userId = String.valueOf(settlementRequestLog.getUser().getId()); + + // ์ˆ˜์‹ ์ž์— ์—ฐ๊ฒฐ๋œ ๋ชจ๋“  SseEmitter ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ด + Map emitters = + emitterRepository.findAllEmitterStartWithByUserId(userId); + + // eventId ์ƒ์„ฑ + String eventId = userId + "_" + System.currentTimeMillis(); + + // emitter๋ฅผ ์ˆœํ™˜ํ•˜๋ฉฐ ๊ฐ SseEmitter ๊ฐ์ฒด์— ์•Œ๋ฆผ ์ „์†ก + emitters.forEach( + (key, sseEmitter) -> { + Map eventData = new HashMap<>(); + eventData.put("title", settlementRequestLog.getLogTitle().getTitle()); // ๋กœ๊ทธ ํƒ€์ดํ‹€ (ex. ์ •์‚ฐ ์š”์ฒญ) + eventData.put("message", settlementRequestLog.getLogMessage()); // ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + eventData.put("link", settlementRequestLog.getLink()); // ์ด๋™ ๋งํฌ + emitterRepository.saveEventCache(key, eventData); + try { + sseEmitter.send(SseEmitter.event().id(eventId).name("message").data(eventData)); + } catch (IOException e) { + emitterRepository.deleteById(key); + } + } + ); + } + } +} diff --git a/src/main/java/withbeetravel/service/settlement/SettlementService.java b/src/main/java/withbeetravel/service/settlement/SettlementService.java new file mode 100644 index 00000000..79f23961 --- /dev/null +++ b/src/main/java/withbeetravel/service/settlement/SettlementService.java @@ -0,0 +1,13 @@ +package withbeetravel.service.settlement; + +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.settlement.ShowSettlementDetailResponse; + +public interface SettlementService { + ShowSettlementDetailResponse getSettlementDetails(Long userId, Long travelId); + void requestSettlement(Long userId, Long travelId); + + String agreeSettlement(Long userId, Long travelId); + + void cancelSettlement(Long userId, Long travelId); +} \ No newline at end of file diff --git a/src/main/java/withbeetravel/service/settlement/SettlementServiceImpl.java b/src/main/java/withbeetravel/service/settlement/SettlementServiceImpl.java new file mode 100644 index 00000000..5e7d5941 --- /dev/null +++ b/src/main/java/withbeetravel/service/settlement/SettlementServiceImpl.java @@ -0,0 +1,578 @@ +package withbeetravel.service.settlement; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.domain.*; +import withbeetravel.dto.response.settlement.*; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.exception.error.BankingErrorCode; +import withbeetravel.exception.error.SettlementErrorCode; +import withbeetravel.exception.error.TravelErrorCode; +import withbeetravel.repository.*; +import withbeetravel.repository.notification.EmitterRepository; +import withbeetravel.service.banking.AccountService; +import withbeetravel.service.notification.SettlementRequestLogService; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +@Service +@RequiredArgsConstructor +@Transactional +public class SettlementServiceImpl implements SettlementService { + + private final TravelMemberRepository travelMemberRepository; + private final SettlementRequestRepository settlementRequestRepository; + private final TravelMemberSettlementHistoryRepository travelMemberSettlementHistoryRepository; + private final PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + private final UserRepository userRepository; + private final TravelRepository travelRepository; + private final SharedPaymentRepository sharedPaymentRepository; + private final SettlementRequestLogRepository settlementRequestLogRepository; + private final AccountRepository accountRepository; + + private final SettlementPendingService settlementPendingService; + private final AccountService accountService; + private final TaskScheduler taskScheduler; + private final SettlementRequestLogService settlementRequestLogService; + private final EmitterRepository emitterRepository; + + + @Override + @Transactional(readOnly = true) + public ShowSettlementDetailResponse getSettlementDetails(Long userId, Long travelId) { + SettlementRequest settlementRequest = findSettlementRequestByTravelId(travelId); + Long settlementRequestId = settlementRequest.getId(); + + Long myTravelMemberId = findMyTravelMemberByTravelIdAndUserId(travelId, userId).getId(); + + List travelMemberSettlementHistories = + travelMemberSettlementHistoryRepository.findAllBySettlementRequestId(settlementRequestId); + + // ๋ฏธ๋™์˜ ์ธ์›์ˆ˜ + int disagreeCount = settlementRequest.getDisagreeCount(); + + ShowMyTotalPaymentResponse myTotalPayments = + createMyTotalPaymentResponse(userId, travelMemberSettlementHistories, myTravelMemberId); + + List others = + createOtherSettlementResponses(travelMemberSettlementHistories, myTravelMemberId); + + MyDetailPaymentResponse myDetailPaymentResponse = createMyDetailPaymentResponses(myTravelMemberId); + List myDetailPayments = myDetailPaymentResponse.getMyDetailPaymentResponses(); + int totalPaymentAmounts = myDetailPaymentResponse.getTotalPaymentAmounts(); + int totalRequestedAmounts = myDetailPaymentResponse.getTotalRequestedAmounts(); + + return ShowSettlementDetailResponse.of( + myTotalPayments, disagreeCount, totalPaymentAmounts, totalRequestedAmounts, myDetailPayments, others); + } + + @Override + public void requestSettlement(Long userId, Long travelId) { + TravelMember travelMember = findMyTravelMemberByTravelIdAndUserId(travelId, userId); + validateIsCaptain(travelMember); + + // ์—ฌํ–‰ ๋ฉค๋ฒ„ ์ธ์›์ˆ˜ ์นด์šดํŠธ + int totalMemberCount = travelMemberRepository.findAllByTravelId(travelId).size(); + + Travel travel = findTravelById(travelId); + + // ์ง„ํ–‰ ์ค‘์ธ ์ •์‚ฐ ์š”์ฒญ์ด ์žˆ์„ ๊ฒฝ์šฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + validateSettlementStatusIsNotOngoing(travel); + + // ์ •์‚ฐ ์š”์ฒญ ์ƒ์„ฑ + SettlementRequest newSettlementRequest = createSettlementRequest(travel, totalMemberCount); + + // ์—ฌํ–‰๋ฉค๋ฒ„์ •์‚ฐ๋‚ด์—ญ ์ƒ์„ฑ + for (TravelMember member : travelMemberRepository.findAllByTravelId(travelId)) { + Long travelMemberId = member.getId(); + User user = member.getUser(); + + int ownPaymentCost = getTotalOwnPaymentCost(travelMemberId); + int actualBurdenCost = getTotalActualBurdenCost(travelMemberId); + + TravelMemberSettlementHistory travelMemberSettlementHistory = + TravelMemberSettlementHistory.builder() + .settlementRequest(newSettlementRequest) + .travelMember(member) + .ownPaymentCost(ownPaymentCost) + .actualBurdenCost(actualBurdenCost) + .isAgreed(false) + .build(); + + travelMemberSettlementHistoryRepository.save(travelMemberSettlementHistory); + + // ์—ฌํ–‰์žฅ ์ œ์™ธํ•˜๊ณ  ์ •์‚ฐ ์š”์ฒญ ์ „์†ก + if (user.getId() != userId) { + // ์ •์‚ฐ ์š”์ฒญ ์ €์žฅ + saveSettlementRequestLog(travel, user, LogTitle.SETTLEMENT_REQUEST, 0); + } + + } + + // ์ •์‚ฐ ์—ฌ๋ถ€๋ฅผ ONGOING์œผ๋กœ ๋ณ€๊ฒฝ + travel.updateSettlementStatus(SettlementStatus.ONGOING); + + // 24์‹œ๊ฐ„(= 86400์ดˆ) ๋’ค์— createSettlementRerequestLogForNotAgreed ์‹คํ–‰ + try { + if (newSettlementRequest.getRequestStartTime() != null) { + taskScheduler.schedule( + () -> settlementRequestLogService.createSettlementReRequestLogForNotAgreed(newSettlementRequest), + newSettlementRequest.getRequestStartTime() + .atZone(ZoneId.systemDefault()).toInstant().plusSeconds(24 * 60 * 60)); + } + } catch (Exception e) { + throw new CustomException(SettlementErrorCode.SCHEDULER_PROCESSING_FAILED); + } + } + + private void validateSettlementStatusIsNotOngoing(Travel travel) { + if (travel.getSettlementStatus().equals(SettlementStatus.ONGOING)) { + throw new CustomException(SettlementErrorCode.SETTLEMENT_ONGOING_ALREADY_EXISTS); + } + } + + @Override + public String agreeSettlement(Long userId, Long travelId) { + // ํ•ด๋‹น ๋ฉค๋ฒ„๊ฐ€ ์—ฌํ–‰ ๋ฉค๋ฒ„์ธ์ง€ ํ™•์ธ + TravelMember selfTravelMember = findMyTravelMemberByTravelIdAndUserId(travelId, userId); + + // ์ •์‚ฐ ์š”์ฒญ์˜ ์กด์žฌ ์—ฌ๋ถ€ ๋ฐ ์ •์‚ฐ ์—ฌ๋ถ€๊ฐ€ ONGOING(์ง„ํ–‰์ค‘)์ธ์ง€ ํ™•์ธ + SettlementRequest settlementRequest = findSettlementRequestByTravelId(travelId); + Travel travel = findTravelById(travelId); + validateSettlementRequestOngoing(travel); + + // ํ•ด๋‹น ๋ฉค๋ฒ„๊ฐ€ ์ด๋ฏธ ๋™์˜ํ•œ ์ •์‚ฐ ์š”์ฒญ์ธ์ง€ ํ™•์ธ + TravelMemberSettlementHistory travelMemberSettlementHistory = + travelMemberSettlementHistoryRepository + .findTravelMemberSettlementHistoryBySettlementRequestIdAndTravelMemberId( + settlementRequest.getId(), selfTravelMember.getId()); + validateSettlementRequestAlreadyAgree(travelMemberSettlementHistory); + + // ๋‚˜์˜ ์ด ์ •์‚ฐ ๊ธˆ์•ก์ด ๋งˆ์ด๋„ˆ์Šค์ธ ๊ฒฝ์šฐ, ์ž”์•ก์ด ๋ถ€์กฑํ•˜์ง€ ์•Š์€ ์ง€ ํ™•์ธ + User user = validateUser(userId); + int myTotalPaymentCost = + travelMemberSettlementHistory.getOwnPaymentCost() - travelMemberSettlementHistory.getActualBurdenCost(); + Account myConnectedAccount = user.getConnectedAccount(); + validateMyBalanceIsEnough(myTotalPaymentCost, myConnectedAccount); + + // disagree_count๊ฐ€ 2 ์ด์ƒ์ด๋ฉด isAgreed๋ฅผ true๋กœ ๋ณ€๊ฒฝ, 1์ด๋ฉด ์ •์‚ฐ ์ง„ํ–‰, 0์ด๋ฉด ์—๋Ÿฌ ๋ฐœ์ƒ + int disagreeCount = settlementRequest.getDisagreeCount(); + + if (disagreeCount >= 2) { + updateIsAgreedAndDisagreeCount(travelMemberSettlementHistory, settlementRequest); + + return "์ •์‚ฐ ๋™์˜ ์™„๋ฃŒ"; + } else if (disagreeCount == 1) { + + // ์ด ์ •์‚ฐ ๊ธˆ์•ก(ownPaymentCost - actualBurdenCost) ๊ฐ’์˜ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ travelMemberSettlementHistory ๋ฆฌ์ŠคํŠธ ์ •๋ ฌ + List travelMemberSettlementHistories = + travelMemberSettlementHistoryRepository + .findAllBySettlementRequestIdOrderByCalculatedCost(settlementRequest.getId()); + + // ์ด ์ •์‚ฐ ๊ธˆ์•ก์ด ๋งˆ์ด๋„ˆ์Šค์ธ ์‚ฌ๋žŒ๋งŒ ์ž”์•ก์ด ์žˆ๋Š”์ง€ ํ™•์ธ + // insufficientBalanceMembers : ์ž”์•ก์ด ๋ถ€์กฑํ•œ ์—ฌํ–‰๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ + List insufficientBalanceMembers = new ArrayList<>(); + validateOtherMembersBalance(travelMemberSettlementHistories, insufficientBalanceMembers); + + // insufficientBalancedMembers์— ํ•œ ๋ช…์ด๋ผ๋„ ์žˆ์„ ๊ฒฝ์šฐ, ์ •์‚ฐ ๋ณด๋ฅ˜ + if (!insufficientBalanceMembers.isEmpty()) { + + List settlementRequestLogs = insufficientBalanceMembers.stream() + .map(travelMember -> { + User insufficientUser = travelMember.getUser(); + return saveSettlementRequestLog(travel, insufficientUser, LogTitle.SETTLEMENT_PENDING, 0); + }) + .toList(); + + // ๋กค๋ฐฑ์‹œ ์‹คํ–‰๋˜๋„๋ก ๋‹ค๋ฅธ ์„œ๋น„์Šค ํด๋ž˜์Šค๋กœ ๋ถ„๋ฆฌ + settlementPendingService.handlePendingSettlementRequest( + settlementRequestLogs, + insufficientBalanceMembers, + settlementRequest, + insufficientBalanceMembers.size(), + travelMemberSettlementHistory); + + throw new CustomException(SettlementErrorCode.SETTLEMENT_INSUFFICIENT_BALANCE); + } + + // ๋‚˜์˜ ์ •์‚ฐ ๋™์˜ ์—ฌ๋ถ€๋ฅผ false -> true๋กœ ๋ณ€๊ฒฝ, disagreeCount์—์„œ -1ํ•˜๊ธฐ (0์œผ๋กœ ๋จ) + updateIsAgreedAndDisagreeCount(travelMemberSettlementHistory, settlementRequest); + + // totalPaymentCost๊ฐ€ 0๋ณด๋‹ค ์ž‘์€ ๊ฒฝ์šฐ, ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ๊ณ„์ขŒ์—์„œ totalPaymentCost๋ฅผ ์ธ์ถœ + // totalPaymentCost๊ฐ€ 0 ์ด์ƒ์ผ ๊ฒฝ์šฐ, ํ•ด๋‹น ๋ฉค๋ฒ„์˜ ๊ณ„์ขŒ์— totalPaymentCost๋ฅผ ์†ก๊ธˆ + Account managerAccount = accountRepository + .findById(1L).orElseThrow(() -> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + // ์ด ์ •์‚ฐ ๊ธˆ์•ก์˜ ํ•ฉ์‚ฐ ๊ณ„์‚ฐ + int totalPaymentCostSum = getTotalPaymentCostSum(travelMemberSettlementHistories); + + // ์œ„๋น„ํŠธ๋ž˜๋ธ” ๊ณ„์ขŒ(๊ด€๋ฆฌ์ž ๊ณ„์ขŒ)์˜ ์ž”์•ก ๋ถ€์กฑ ํ™•์ธ + validateManagerBalance(managerAccount, totalPaymentCostSum); + + // ์ •์‚ฐ ์ฒ˜๋ฆฌ + processSettlement(travelMemberSettlementHistories, managerAccount, travel); + + // ์ •์‚ฐ ์—ฌ๋ถ€๋ฅผ DONE์œผ๋กœ ๋ณ€๊ฒฝ + travel.updateSettlementStatus(SettlementStatus.DONE); + + // ์ •์‚ฐ ์ข…๋ฃŒ์ผ์„ ํ˜„์žฌ๋กœ ๋ณ€๊ฒฝ + settlementRequest.updateRequestEndDate(LocalDateTime.now()); + + // ์ •์‚ฐ ์™„๋ฃŒ ๋กœ๊ทธ ์ƒ์„ฑ + for (TravelMember travelMember : travelMemberRepository.findAllByTravelId(travelId)) { + SettlementRequestLog settlementRequestLog = + saveSettlementRequestLog(travel, travelMember.getUser(), LogTitle.SETTLEMENT_COMPLETE, totalPaymentCostSum); + settlementRequestLogRepository.save(settlementRequestLog); + } + + return "๋ชจ๋“  ์—ฌํ–‰ ๋ฉค๋ฒ„์˜ ์ •์‚ฐ ๋™์˜ ์™„๋ฃŒ ํ›„ ์ •์‚ฐ ์™„๋ฃŒ"; + } + + // ์ •์‚ฐ ๋ฏธ๋™์˜ ์ธ์›์ˆ˜๊ฐ€ 0์ธ ๊ฒฝ์šฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + else { + throw new CustomException(SettlementErrorCode.SETTLEMENT_DISAGREE_COUNT_NOT_CERTAIN); + } + } + + private User validateUser(Long userId) { + return userRepository.findById(userId).orElseThrow(() -> new CustomException(AuthErrorCode.AUTHENTICATION_FAILED)); + } + + private void processSettlement(List travelMemberSettlementHistories, Account managerAccount, Travel travel) { + for (TravelMemberSettlementHistory settlementHistory : travelMemberSettlementHistories) { + int totalPaymentCost = settlementHistory.getOwnPaymentCost() - settlementHistory.getActualBurdenCost(); + User user = settlementHistory.getTravelMember().getUser(); + Account connectedAccount = user.getConnectedAccount(); + if (totalPaymentCost < 0) { + accountService.transfer(connectedAccount.getId(), + managerAccount.getAccountNumber(), -totalPaymentCost, "์œ„๋น„ํŠธ๋ž˜๋ธ” ์ •์‚ฐ๊ธˆ ์ถœ๊ธˆ"); + } else { + // ๊ด€๋ฆฌ์ž ๊ณ„์ขŒ์˜ ์†ก๊ธˆ ๋ฉ”์‹œ์ง€๋ฅผ "{์—ฌํ–‰๋ช…}์˜ ์ •์‚ฐ๊ธˆ ์ถœ๊ธˆ"์œผ๋กœ ํ‘œ์‹œ + accountService.transfer(managerAccount.getId(), + connectedAccount.getAccountNumber(), totalPaymentCost, travel.getTravelName() + "์˜ ์ •์‚ฐ๊ธˆ ์ถœ๊ธˆ"); + } + } + } + + private int getTotalPaymentCostSum(List travelMemberSettlementHistories) { + int totalPaymentCostSum = 0; + for (TravelMemberSettlementHistory settlementHistory : travelMemberSettlementHistories) { + totalPaymentCostSum += settlementHistory.getOwnPaymentCost() - settlementHistory.getActualBurdenCost(); + } + return totalPaymentCostSum; + } + + private void validateManagerBalance(Account managerAccount, int totalPaymentCostSum) { + if (managerAccount.getBalance() < totalPaymentCostSum) { + throw new CustomException(BankingErrorCode.INSUFFICIENT_MANAGER_ACCOUNT_BALANCE); + } + } + + @Override + public void cancelSettlement(Long userId, Long travelId) { + + // ์ •์‚ฐ ์š”์ฒญ์ด ์žˆ๋Š”์ง€ ํ™•์ธ + SettlementRequest settlementRequest = findSettlementRequestByTravelId(travelId); + + // ์ง„ํ–‰ ์ค‘์ธ ์ •์‚ฐ์ธ์ง€ ํ™•์ธ + Travel travel = findTravelById(travelId); + validateSettlementRequestOngoing(travel); + + // ๋ฉค๋ฒ„๋“ค์˜ ์ •์‚ฐ ๋‚ด์—ญ์„ ๋จผ์ € ์‚ญ์ œ + travelMemberSettlementHistoryRepository.deleteAllBySettlementRequestId(settlementRequest.getId()); + + // ๋ฉค๋ฒ„๋“ค์˜ settlementHistory ์ดˆ๊ธฐํ™” + List travelMembers = travelMemberRepository.findAllByTravelId(travelId); + for (int i = 0; i < travelMembers.size(); i++) { + travelMembers.get(i).initializeSettlementHistory(); + } + + // ์ •์‚ฐ ์š”์ฒญ ์‚ญ์ œ + settlementRequestRepository.deleteById(settlementRequest.getId()); + + // ์ •์‚ฐ ์—ฌ๋ถ€๋ฅผ ONGOING -> PENDING์œผ๋กœ ๋ณ€๊ฒฝ + travel.updateSettlementStatus(SettlementStatus.PENDING); + + // ์ •์‚ฐ ์ทจ์†Œ ๋กœ๊ทธ ์ €์žฅ + for (TravelMember travelMember : travelMemberRepository.findAllByTravelId(travelId)) { + saveSettlementRequestLog(travel, travelMember.getUser(), LogTitle.SETTLEMENT_CANCEL, 0); + } + } + + private void updateIsAgreedAndDisagreeCount(TravelMemberSettlementHistory travelMemberSettlementHistory, SettlementRequest settlementRequest) { + travelMemberSettlementHistory.updateIsAgreed(true); + settlementRequest.updateDisagreeCount(-1); + } + + private void validateOtherMembersBalance(List travelMemberSettlementHistories, List insufficientBalanceMembers) { + for (TravelMemberSettlementHistory settlementHistory : travelMemberSettlementHistories) { + int totalPaymentCost = settlementHistory.getOwnPaymentCost() - settlementHistory.getActualBurdenCost(); + + if (totalPaymentCost < 0) { + TravelMember travelMember = settlementHistory.getTravelMember(); + Account connectedAccount = travelMember.getUser().getConnectedAccount(); + + if (connectedAccount.getBalance() + totalPaymentCost < 0) { + insufficientBalanceMembers.add(travelMember); + } + } + } + } + + private void validateMyBalanceIsEnough(int myTotalPaymentCost, Account myConnectedAccount) { + if (myTotalPaymentCost < 0) { + if (myConnectedAccount.getBalance() + myTotalPaymentCost < 0) { + throw new CustomException(BankingErrorCode.INSUFFICIENT_FUNDS); + } + } + } + + + private void validateSettlementRequestAlreadyAgree(TravelMemberSettlementHistory travelMemberSettlementHistory) { + if (travelMemberSettlementHistory.isAgreed()) { + throw new CustomException(SettlementErrorCode.SETTLEMENT_ALREADY_AGREED); + } + } + + private void validateSettlementRequestOngoing(Travel travel) { + if (!travel.getSettlementStatus().equals(SettlementStatus.ONGOING)) { + throw new CustomException(SettlementErrorCode.SETTLEMENT_NOT_ONGOING); + } + } + + private SettlementRequestLog saveSettlementRequestLog(Travel travel, User user, + LogTitle logTitle, int additionalValue) { + + String logMessage = getLogMessage(travel, user, logTitle, additionalValue); + String link = getLink(travel, user, logTitle); + + SettlementRequestLog settlementRequestLog = settlementRequestLogRepository.save( + SettlementRequestLog.builder() + .travel(travel) + .user(user) + .logTitle(logTitle) + .logMessage(logMessage) + .link(link) + .build()); + + // ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ์ „์†ก + if (!logTitle.equals(LogTitle.SETTLEMENT_PENDING)) { + sendNotification(settlementRequestLog); + } + + return settlementRequestLog; + } + + @Nullable + private String getLink(Travel travel, User user, LogTitle logTitle) { + return logTitle.equals(LogTitle.SETTLEMENT_PENDING) || logTitle.equals(LogTitle.SETTLEMENT_COMPLETE) ? + logTitle.getLinkPattern(user.getConnectedAccount().getId()) : + (logTitle.equals(LogTitle.SETTLEMENT_REQUEST) ? + logTitle.getLinkPattern(travel.getId()) : null); + } + + @NotNull + private String getLogMessage(Travel travel, User user, LogTitle logTitle, int additionalValue) { + return logTitle.equals(LogTitle.SETTLEMENT_PENDING) ? + logTitle.getMessage(travel.getTravelName(), user.getName()) : + (logTitle.equals(LogTitle.SETTLEMENT_COMPLETE) ? + logTitle.getMessage(travel.getTravelName(), additionalValue) : + logTitle.getMessage(travel.getTravelName())); + } + + private void sendNotification(SettlementRequestLog settlementRequestLog) { + String userId = String.valueOf(settlementRequestLog.getUser().getId()); + + // ์ˆ˜์‹ ์ž์— ์—ฐ๊ฒฐ๋œ ๋ชจ๋“  SseEmitter ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ด + Map emitters = + emitterRepository.findAllEmitterStartWithByUserId(userId); + + // eventId ์ƒ์„ฑ + String eventId = userId + "_" + System.currentTimeMillis(); + + // emitter๋ฅผ ์ˆœํ™˜ํ•˜๋ฉฐ ๊ฐ SseEmitter ๊ฐ์ฒด์— ์•Œ๋ฆผ ์ „์†ก + emitters.forEach( + (key, sseEmitter) -> { + Map eventData = new HashMap<>(); + eventData.put("title", settlementRequestLog.getLogTitle().getTitle()); // ๋กœ๊ทธ ํƒ€์ดํ‹€ (ex. ์ •์‚ฐ ์š”์ฒญ) + eventData.put("message", settlementRequestLog.getLogMessage()); // ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + eventData.put("link", settlementRequestLog.getLink()); // ์ด๋™ ๋งํฌ + emitterRepository.saveEventCache(key, eventData); + try { + sseEmitter.send(SseEmitter.event().id(eventId).name("message").data(eventData)); + } catch (IOException e) { + emitterRepository.deleteById(key); + } + } + ); + } + + private int getTotalActualBurdenCost(Long travelMemberId) { + int actualBurdenCost = 0; + for (PaymentParticipatedMember paymentParticipatedMember : + paymentParticipatedMemberRepository.findAllByTravelMemberId(travelMemberId)) { + SharedPayment sharedPayment = paymentParticipatedMember.getSharedPayment(); + actualBurdenCost += sharedPayment.getPaymentAmount() / sharedPayment.getParticipantCount(); + } + return actualBurdenCost; + } + + private int getTotalOwnPaymentCost(Long travelMemberId) { + int ownPaymentCost = 0; + for (SharedPayment sharedPayment : + sharedPaymentRepository.findAllByAddedByMemberId(travelMemberId)) { + ownPaymentCost += sharedPayment.getPaymentAmount(); + } + return ownPaymentCost; + } + + private SettlementRequest createSettlementRequest(Travel travel, int totalMemberCount) { + return settlementRequestRepository.save( + SettlementRequest.builder() + .travel(travel) + .disagreeCount(totalMemberCount) + .build()); + } + + private Travel findTravelById(Long travelId) { + return travelRepository + .findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + } + + private void validateIsCaptain(TravelMember travelMember) { + if (!travelMember.isCaptain()) { + throw new CustomException(SettlementErrorCode.NO_PERMISSION_TO_MANAGE_SETTLEMENT); + } + } + + private MyDetailPaymentResponse createMyDetailPaymentResponses(Long myTravelMemberId) { + // ๋‚ด๊ฐ€ ์ •์‚ฐ์— ํฌํ•จ๋œ ๊ฒฐ์ œ ๋‚ด์—ญ๋“ค + List paymentParticipatedMembers = paymentParticipatedMemberRepository.findAllByTravelMemberId(myTravelMemberId); + List settledPayments = paymentParticipatedMembers.stream() + .map(PaymentParticipatedMember::getSharedPayment).toList(); + + // ๋‚ด๊ฐ€ ๊ฒฐ์ œํ•œ ๋ฆฌ์ŠคํŠธ + List sharedPayments = sharedPaymentRepository.findAllByAddedByMemberId(myTravelMemberId); + // ๋‚ด๊ฐ€ ๊ฒฐ์ œํ–ˆ์ง€๋งŒ ์ •์‚ฐ์— ํฌํ•จ๋˜์ง€ ์•Š๋Š” ๊ณต๋™๊ฒฐ์ œ๋‚ด์—ญ๋“ค + List unsettledPayments = sharedPayments.stream() + .filter(sharedPayment -> { + Long sharedPaymentId = sharedPayment.getId(); + return !paymentParticipatedMemberRepository.existsByTravelMemberIdAndSharedPaymentId(myTravelMemberId, sharedPaymentId); + }).toList(); + + // settledPayments + unsettledPayments + List allPayments = Stream.concat(settledPayments.stream(), unsettledPayments.stream()).toList(); + + // ์ด ๋ฐ›์„ ๊ธˆ์•ก + int sumOfPaymentAmounts = 0; + + // ์ด ๋ณด๋‚ผ ๊ธˆ์•ก + int sumOfRequestedAmounts = 0; + + // ์„ธ๋ถ€ ์ง€์ถœ ๋‚ด์—ญ๋“ค + List myDetailPaymentResponses = new ArrayList<>(); + + for (SharedPayment sharedPayment : allPayments) { + Long sharedPaymentId = sharedPayment.getId(); + TravelMember addedByTravelMember = sharedPayment.getAddedByMember(); + + int participantCount = sharedPayment.getParticipantCount(); + int paymentAmount = sharedPayment.getPaymentAmount(); + int amountPerPerson = paymentAmount / participantCount; + + // ๋‚ด๊ฐ€ ๊ฒฐ์ œํ–ˆ๊ณ , ์ •์‚ฐ์— ํฌํ•จ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + boolean included = paymentParticipatedMemberRepository + .existsByTravelMemberIdAndSharedPaymentId( + addedByTravelMember.getId(), sharedPayment.getId()); + + // ๋‚ด๊ฐ€ ๊ฒฐ์ œํ•œ ๋‚ด์—ญ, ๋‚ด๊ฐ€ ๊ฒฐ์ œํ–ˆ์ง€๋งŒ ์ •์‚ฐ์— ํฌํ•จ๋˜์ง€ ์•Š์€ ๋‚ด์—ญ, + // ๋‚ด๊ฐ€ ๊ฒฐ์ œํ•˜์ง€ ์•Š์•˜์ง€๋งŒ ์ •์‚ฐ์— ํฌํ•จ๋œ ๋‚ด์—ญ์„ ๊ตฌ๋ถ„ํ•ด์„œ RequestedAmount ๊ณ„์‚ฐ + int requestedAmount = + addedByTravelMember.getId() == myTravelMemberId ? + (included ? paymentAmount - amountPerPerson : paymentAmount) : -amountPerPerson; + + if (requestedAmount < 0) { + sumOfRequestedAmounts += requestedAmount; + } else { + sumOfPaymentAmounts += requestedAmount; + + } + + myDetailPaymentResponses.add(ShowMyDetailPaymentResponse.of( + sharedPaymentId, + paymentAmount, + requestedAmount, + sharedPayment.getStoreName(), + sharedPayment.getPaymentDate())); + } + + return MyDetailPaymentResponse.of(sumOfPaymentAmounts, -sumOfRequestedAmounts, myDetailPaymentResponses); + } + + private List createOtherSettlementResponses + (List travelMemberSettlementHistories, Long myTravelMemberId) { + return travelMemberSettlementHistories + .stream() + .filter(history -> !history.getTravelMember().getId().equals(myTravelMemberId)) + .map(history -> { + Long travelMemberId = history.getTravelMember().getId(); + boolean isAgreed = history.isAgreed(); + TravelMember travelMember = travelMemberRepository.findById(travelMemberId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN)); + String memberName = travelMember.getUser().getName(); + int totalPaymentCost = history.getOwnPaymentCost() - history.getActualBurdenCost(); + return ShowOtherSettlementResponse.of(travelMemberId, memberName, totalPaymentCost, isAgreed); + }) + .toList(); + } + + private ShowMyTotalPaymentResponse createMyTotalPaymentResponse( + Long userId, List travelMemberSettlementHistories, Long myTravelMemberId) { + + // ๋‚ด๊ฐ€ ๊ฒฐ์ œํ•œ ๊ณต์œ  ๊ฒฐ์ œ ๋‚ด์—ญ์˜ 1/n ๊ธˆ์•ก์˜ ํ•ฉ๊ณ„ + List sharedPayments = sharedPaymentRepository.findAllByAddedByMemberId(myTravelMemberId); + int sumOfAmountPerPerson = sharedPayments.stream() + .mapToInt(sharedPayment -> sharedPayment.getPaymentAmount() / sharedPayment.getParticipantCount()) + .sum(); + + return travelMemberSettlementHistories + .stream() + .filter(history -> history.getTravelMember().getId().equals(myTravelMemberId)) + .findFirst() + .map(history -> { + String name = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.AUTHENTICATION_FAILED)).getName(); + // ๋‚ด๊ฐ€ ๋ฐ›์•„์•ผ ํ•  ๊ธˆ์•ก์˜ ํ•ฉ๊ณ„ = ๋‚ด ๊ฒฐ์ œ ๊ธˆ์•ก ํ•ฉ๊ณ„ - 1/n ๊ธˆ์•ก์˜ ํ•ฉ๊ณ„ + int ownPaymentCost = history.getOwnPaymentCost() - sumOfAmountPerPerson; + int actualBurdenCost = history.getActualBurdenCost() - sumOfAmountPerPerson; + boolean isAgreed = history.isAgreed(); + return ShowMyTotalPaymentResponse.of(name, isAgreed, ownPaymentCost, actualBurdenCost); + }) + .orElseThrow(() -> new CustomException(SettlementErrorCode.MEMBER_SETTLEMENT_HISTORY_NOT_FOUND)); + } + + private TravelMember findMyTravelMemberByTravelIdAndUserId(Long travelId, Long userId) { + return travelMemberRepository + .findByTravelIdAndUserId(travelId, userId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN)); + } + + private SettlementRequest findSettlementRequestByTravelId(Long travelId) { + return settlementRequestRepository.findByTravelId(travelId) + .orElseThrow(() -> new CustomException(SettlementErrorCode.SETTLEMENT_NOT_FOUND)); + } +} diff --git a/src/main/java/withbeetravel/service/travel/HoneyCapsuleService.java b/src/main/java/withbeetravel/service/travel/HoneyCapsuleService.java new file mode 100644 index 00000000..719ae9ca --- /dev/null +++ b/src/main/java/withbeetravel/service/travel/HoneyCapsuleService.java @@ -0,0 +1,11 @@ +package withbeetravel.service.travel; + +import withbeetravel.dto.response.travel.HoneyCapsuleResponse; +import withbeetravel.dto.response.SuccessResponse; + +import java.util.List; + +public interface HoneyCapsuleService { + + SuccessResponse> getHoneyCapsuleList(Long travelId); +} diff --git a/src/main/java/withbeetravel/service/travel/HoneyCapsuleServiceImpl.java b/src/main/java/withbeetravel/service/travel/HoneyCapsuleServiceImpl.java new file mode 100644 index 00000000..62baee62 --- /dev/null +++ b/src/main/java/withbeetravel/service/travel/HoneyCapsuleServiceImpl.java @@ -0,0 +1,55 @@ +package withbeetravel.service.travel; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.SettlementStatus; +import withbeetravel.domain.SharedPayment; +import withbeetravel.domain.Travel; +import withbeetravel.dto.response.travel.HoneyCapsuleResponse; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.SettlementErrorCode; +import withbeetravel.exception.error.TravelErrorCode; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class HoneyCapsuleServiceImpl implements HoneyCapsuleService{ + + private final TravelRepository travelRepository; + private final SharedPaymentRepository sharedPaymentRepository; + + + @Override + @Transactional(readOnly = true) + public SuccessResponse> getHoneyCapsuleList(Long travelId) { + + // travelId์— ๋Œ€ํ•œ ์—”ํ‹ฐํ‹ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + Travel travel = travelRepository.findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + + // ์—ฌํ–‰ ์ •์‚ฐ์ด ์•„์ง ์•ˆ๋๋‚ฌ๋‹ค๋ฉด, ํ—ˆ๋‹ˆ์บก์А์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Œ + if(travel.getSettlementStatus() != SettlementStatus.DONE) { + throw new CustomException(SettlementErrorCode.SETTLEMENT_NOT_COMPLETED); + } + + // travelId์˜ ๊ณต๋™ ๊ฒฐ์ œ ๋‚ด์—ญ ๋ชจ๋‘ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ + List allByTravelId = sharedPaymentRepository.findAllByTravelId(travelId); + + // SharedPayment ๋ฆฌ์ŠคํŠธ๋ฅผ HoneyCapsuleResponse ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ (์‚ฌ์ง„๊ณผ ๋ฌธ๊ตฌ๊ฐ€ ๋‘˜ ๋‹ค ์—†์œผ๋ฉด ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Œ) + List honeyCapsuleResponseList = new ArrayList<>(); + for(SharedPayment sharedPayment : allByTravelId) { + if(sharedPayment.getPaymentImage() != null || sharedPayment.getPaymentComment() != null) { + honeyCapsuleResponseList.add(HoneyCapsuleResponse.from(sharedPayment)); + } + } + + return SuccessResponse.of(200, "์—ฌํ–‰ ๊ธฐ๋ก ์กฐํšŒ ์„ฑ๊ณต", honeyCapsuleResponseList); + } +} diff --git a/src/main/java/withbeetravel/service/travel/TravelMemberService.java b/src/main/java/withbeetravel/service/travel/TravelMemberService.java new file mode 100644 index 00000000..127238e3 --- /dev/null +++ b/src/main/java/withbeetravel/service/travel/TravelMemberService.java @@ -0,0 +1,12 @@ +package withbeetravel.service.travel; + +import withbeetravel.domain.TravelMember; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.travel.TravelMemberResponse; + +import java.util.List; + +public interface TravelMemberService { + + List getTravelMembers(Long travelId); +} diff --git a/src/main/java/withbeetravel/service/travel/TravelMemberServiceImpl.java b/src/main/java/withbeetravel/service/travel/TravelMemberServiceImpl.java new file mode 100644 index 00000000..fc518311 --- /dev/null +++ b/src/main/java/withbeetravel/service/travel/TravelMemberServiceImpl.java @@ -0,0 +1,25 @@ +package withbeetravel.service.travel; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import withbeetravel.domain.TravelMember; +import withbeetravel.dto.response.SuccessResponse; +import withbeetravel.dto.response.travel.TravelMemberResponse; +import withbeetravel.repository.TravelMemberRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class TravelMemberServiceImpl implements TravelMemberService { + + private final TravelMemberRepository travelMemberRepository; + + @Override + @Transactional(readOnly = true) + public List getTravelMembers(Long travelId) { + return travelMemberRepository.findAllByTravelId(travelId); + } +} diff --git a/src/main/java/withbeetravel/service/travel/TravelService.java b/src/main/java/withbeetravel/service/travel/TravelService.java new file mode 100644 index 00000000..a88d1ccf --- /dev/null +++ b/src/main/java/withbeetravel/service/travel/TravelService.java @@ -0,0 +1,35 @@ +package withbeetravel.service.travel; + +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.dto.request.account.CardCompletedRequest; +import withbeetravel.dto.response.account.AccountConnectedWibeeResponse; +import withbeetravel.dto.response.travel.TravelHomeResponse; +import withbeetravel.dto.request.travel.InviteCodeSignUpRequest; +import withbeetravel.dto.request.travel.TravelRequest; +import withbeetravel.dto.response.travel.InviteCodeGetResponse; +import withbeetravel.dto.response.travel.InviteCodeSignUpResponse; +import withbeetravel.dto.response.travel.TravelResponse; +import withbeetravel.dto.response.travel.TravelListResponse; + +import java.util.List; + +public interface +TravelService { + TravelResponse saveTravel(TravelRequest request,Long userId); + + void editTravel(TravelRequest request, Long travelId); + + TravelHomeResponse getTravel(Long travelId, Long userId); + + InviteCodeSignUpResponse signUpTravel(InviteCodeSignUpRequest request,Long userId); + + InviteCodeGetResponse getInviteCode(Long travelId); + + List getTravelList(Long userId); + + void postConnectedAccount(CardCompletedRequest request, Long userId); + + AccountConnectedWibeeResponse getConnectedWibee(Long userId); + + void changeMainImage(Long travelId, MultipartFile image); +} diff --git a/src/main/java/withbeetravel/service/travel/TravelServiceImpl.java b/src/main/java/withbeetravel/service/travel/TravelServiceImpl.java new file mode 100644 index 00000000..ca20f963 --- /dev/null +++ b/src/main/java/withbeetravel/service/travel/TravelServiceImpl.java @@ -0,0 +1,382 @@ +package withbeetravel.service.travel; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import withbeetravel.domain.*; +import withbeetravel.dto.request.account.CardCompletedRequest; +import withbeetravel.dto.response.account.AccountConnectedWibeeResponse; +import withbeetravel.dto.response.travel.TravelHomeResponse; +import withbeetravel.dto.request.travel.InviteCodeSignUpRequest; +import withbeetravel.dto.request.travel.TravelRequest; +import withbeetravel.dto.response.travel.InviteCodeGetResponse; +import withbeetravel.dto.response.travel.InviteCodeSignUpResponse; +import withbeetravel.dto.response.travel.TravelResponse; +import withbeetravel.dto.response.travel.TravelListResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.exception.error.BankingErrorCode; +import withbeetravel.exception.error.TravelErrorCode; +import withbeetravel.exception.error.ValidationErrorCode; +import withbeetravel.repository.AccountRepository; +import withbeetravel.repository.TravelCountryRepository; +import withbeetravel.repository.TravelRepository; +import withbeetravel.repository.*; +import withbeetravel.repository.notification.EmitterRepository; +import withbeetravel.service.global.S3Uploader; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional +public class TravelServiceImpl implements TravelService { + + + private static final Logger log = LoggerFactory.getLogger(TravelServiceImpl.class); + private final TravelRepository travelRepository; + private final TravelCountryRepository travelCountryRepository; + private final AccountRepository accountRepository; + private final SharedPaymentRepository sharedPaymentRepository; + private final TravelMemberRepository travelMemberRepository; + private final UserRepository userRepository; + private final PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + private final SettlementRequestLogRepository settlementRequestLogRepository; + private final EmitterRepository emitterRepository; + + private final S3Uploader s3Uploader; + + // S3์— ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ๊ฒฝ๋กœ + private static final String TRAVEL_IMAGE_DIR = "travels/"; + + @Override + public TravelResponse saveTravel(TravelRequest requestDto,Long userId) { + + List accounts = accountRepository.findByUserId(userId); + System.out.println(accounts); + boolean hasConnectedWibeeCard = accounts.stream() + .anyMatch(Account::isConnectedWibeeCard); + System.out.println(hasConnectedWibeeCard); + if(!hasConnectedWibeeCard){ + throw new CustomException(TravelErrorCode.TRAVEL_CAPTAIN_NOT); + } + + // ์ดˆ๋Œ€ ์ฝ”๋“œ ์ƒ์„ฑ + String inviteCode = UUID.randomUUID().toString(); + + + + // Travel ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ + Travel travel = Travel.builder() + .travelName(requestDto.getTravelName()) + .travelStartDate(LocalDate.parse(requestDto.getTravelStartDate())) + .travelEndDate(LocalDate.parse(requestDto.getTravelEndDate())) + .isDomesticTravel(requestDto.isDomesticTravel()) + .settlementStatus(SettlementStatus.PENDING) + .inviteCode(inviteCode) + .mainImage(null) + .build(); + + System.out.println("์—ฌํ–‰ ๋ฐ์ดํ„ฐ" + travel); + Travel savedTravel = travelRepository.save(travel); // Travel ์—”ํ‹ฐํ‹ฐ ์ €์žฅ + + // TravelMember ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ (์ƒ์„ฑ์ž๋ฅผ Travel์˜ Captain์œผ๋กœ ์ถ”๊ฐ€) + TravelMember travelMember = TravelMember.builder() + .travel(savedTravel) + .user(userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND))) // ์œ ์ € ๊ฒ€์ฆ + .isCaptain(true) // Captain ์—ญํ• ๋กœ ์ง€์ • + .build(); + + travelMemberRepository.save(travelMember); // TravelMember ์—”ํ‹ฐํ‹ฐ ์ €์žฅ + + // ํ•ด์™ธ ์—ฌํ–‰์ผ ๊ฒฝ์šฐ, ์„ ํƒ๋œ ๋‚˜๋ผ๋“ค์— ๋Œ€ํ•ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ ํ›„ TravelCountry ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ + // TravelCountry ๋ฆฌ์ŠคํŠธ๋ฅผ ๋นˆ ๋ฆฌ์ŠคํŠธ๋กœ ์ดˆ๊ธฐํ™” + List travelCountries = List.of(); + if (!requestDto.isDomesticTravel()) { + travelCountries = requestDto.getTravelCountries().stream() + .map(countryName -> { + // Country enum์— ์กด์žฌํ•˜๋Š”์ง€ ๊ฒ€์ฆ + Country country = Country.findByName(countryName); + return TravelCountry.builder() + .country(country) + .travel(savedTravel) + .build(); + }) + .collect(Collectors.toList()); + + // TravelCountry ์—”ํ‹ฐํ‹ฐ ์ €์žฅ + travelCountryRepository.saveAll(travelCountries); + } + + // ResponseDto ์ƒ์„ฑ ๋ฐ ๋ฐ˜ํ™˜ + TravelResponse travelResponseDto = TravelResponse.from(savedTravel, travelCountries); + + return travelResponseDto; + } + + @Override + public void editTravel(TravelRequest requestDto, Long travelId){ + Travel travel = travelRepository.findById(travelId) + .orElseThrow(() -> new IllegalArgumentException("Travel not found with ID : " + travelId)); + + travel.updateTravel(requestDto.getTravelName(), + LocalDate.parse(requestDto.getTravelStartDate()), + LocalDate.parse(requestDto.getTravelEndDate()), + requestDto.isDomesticTravel()); + + travelCountryRepository.deleteByTravel(travel); + + if(!requestDto.isDomesticTravel()){ + + List updatedTravelCountries = requestDto.getTravelCountries().stream() + .map(countryName -> { + Country country = Country.findByName(countryName); + return TravelCountry.builder().country(country).travel(travel).build(); + }).toList(); + + travelCountryRepository.saveAll(updatedTravelCountries); + } + } + + @Override + public InviteCodeSignUpResponse signUpTravel(InviteCodeSignUpRequest requestDto,Long userId){ + String inviteCode = requestDto.getInviteCode(); + + Travel travel = travelRepository.findByInviteCode(inviteCode). + orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_INVITECODE_NOT)); + + Long travelId = travel.getId(); + int curMemberCount = travelMemberRepository.countByTravelId(travelId); + + + if(curMemberCount >= 10){ + throw new CustomException(TravelErrorCode.TRAVEL_MEMBER_LIMIT); + } + + boolean userAlreadyMember = travelMemberRepository.existsByTravelIdAndUserId(travelId, userId); + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND)); + + if (userAlreadyMember) { + throw new CustomException(TravelErrorCode.TRAVEL_USER_ALREADY_MEMBER); + } + + // ๊ธฐ์กด์˜ ์—ฌํ–‰ ๋ฉค๋ฒ„ ๋ฆฌ์ŠคํŠธ + List travelMembers = travelMemberRepository.findAllByTravelId(travelId); + + TravelMember newMember = TravelMember.builder() + .travel(travel) + .user(user) + .isCaptain(false) // ์ดˆ๋Œ€ํ•œ ์‚ฌ๋žŒ์€ Captain์ด ์•„๋‹˜ + .build(); + + + travelMemberRepository.save(newMember); + + // ํ˜„์žฌ ๊ณต๋™์ง€์ถœ๋‚ด์—ญ ์ฐธ์—ฌ ๋ฉค๋ฒ„๋กœ ์ถ”๊ฐ€ + List sharedPayments = sharedPaymentRepository.findAllByTravelId(travelId); + // PaymentParticipatedMember๊ฐ€ ์—ฐ๊ด€๊ด€๊ณ„์˜ ์ฃผ์ธ + List participatedMembers = sharedPayments.stream() + .map(sharedPayment -> { + // ์ฐธ์—ฌ์ž ์ˆ˜ ์ฆ๊ฐ€ + sharedPayment.updateParticipantCount(sharedPayment.getParticipantCount() + 1); + + return PaymentParticipatedMember.builder() + .travelMember(newMember) + .sharedPayment(sharedPayment) + .build(); + }) + .collect(Collectors.toList()); + + // PaymentParticipatedMember ์ €์žฅ + paymentParticipatedMemberRepository.saveAll(participatedMembers); + + // ๊ธฐ์กด์˜ ์—ฌํ–‰ ๋ฉค๋ฒ„์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก + travelMembers.forEach(travelMember -> + saveLog(travel, travelMember.getUser(), user, LogTitle.TRAVEL_MEMBER_ADDED)); + + return InviteCodeSignUpResponse.builder() + .travelId(travelId) + .build(); + } + + private SettlementRequestLog saveLog(Travel travel, User user, User addedUser, + LogTitle logTitle) { + + String logMessage = logTitle.getMessage(addedUser.getName(), travel.getTravelName()); + String link = logTitle.getLinkPattern(travel.getId()); + + SettlementRequestLog log = settlementRequestLogRepository.save( + SettlementRequestLog.builder() + .travel(travel) + .user(user) + .logTitle(logTitle) + .logMessage(logMessage) + .link(link) + .build()); + + sendNotification(log); + + return log; + } + + private void sendNotification(SettlementRequestLog settlementRequestLog) { + String userId = String.valueOf(settlementRequestLog.getUser().getId()); + + // ์ˆ˜์‹ ์ž์— ์—ฐ๊ฒฐ๋œ ๋ชจ๋“  SseEmitter ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ด + Map emitters = + emitterRepository.findAllEmitterStartWithByUserId(userId); + + // eventId ์ƒ์„ฑ + String eventId = userId + "_" + System.currentTimeMillis(); + + // emitter๋ฅผ ์ˆœํ™˜ํ•˜๋ฉฐ ๊ฐ SseEmitter ๊ฐ์ฒด์— ์•Œ๋ฆผ ์ „์†ก + emitters.forEach( + (key, sseEmitter) -> { + Map eventData = new HashMap<>(); + eventData.put("title", settlementRequestLog.getLogTitle().getTitle()); // ๋กœ๊ทธ ํƒ€์ดํ‹€ (ex. ์ •์‚ฐ ์š”์ฒญ) + eventData.put("message", settlementRequestLog.getLogMessage()); // ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ + eventData.put("link", settlementRequestLog.getLink()); // ์ด๋™ ๋งํฌ + emitterRepository.saveEventCache(key, eventData); + try { + sseEmitter.send(SseEmitter.event().id(eventId).name("message").data(eventData)); + } catch (IOException e) { + emitterRepository.deleteById(key); + } + } + ); + } + + @Override + public TravelHomeResponse getTravel(Long travelId, Long userId) { + // Aspect์—์„œ ์ด๋ฏธ ๊ฒ€์ฆํ–ˆ์œผ๋ฏ€๋กœ Travel์€ ๋ฐ˜๋“œ์‹œ ์กด์žฌ + Travel travel = travelRepository.findById(travelId).get(); + TravelMember travelMember = travelMemberRepository.findByTravelIdAndUserId(travelId, userId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_ACCESS_FORBIDDEN)); + Map statistics = calculateStatistics(travelId); + return TravelHomeResponse.of(travel, travelMember, statistics); + } + + private Map calculateStatistics(Long travelId) { + // ์ง€์ถœ ๋ฐ์ดํ„ฐ ์กฐํšŒ + List expenses = sharedPaymentRepository.findAllByTravelId(travelId); + + // ์ด ์ง€์ถœ์•ก ๊ณ„์‚ฐ + double totalAmount = expenses.stream() + .mapToDouble(SharedPayment::getPaymentAmount) + .sum(); + + // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋น„์œจ ๊ณ„์‚ฐ + return expenses.stream() + .collect(Collectors.groupingBy( + payment -> payment.getCategory().getDescription(), + Collectors.collectingAndThen( + Collectors.summingDouble(SharedPayment::getPaymentAmount), + amount -> Math.round((amount / totalAmount) * 1000.0) / 10.0 // ์†Œ์ˆ˜์  ์ฒซ์งธ์ž๋ฆฌ๊นŒ์ง€ ๋ฐ˜์˜ฌ๋ฆผ + ) + )); + } + + + @Override + public InviteCodeGetResponse getInviteCode(Long travelId){ + Travel travel = travelRepository.findById(travelId).orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + + return new InviteCodeGetResponse(travel.getInviteCode()); + } + + // user์˜ ์—ฌํ–‰ ๋ชฉ๋ก ๋ฆฌ์ŠคํŠธ ์กฐํšŒ + @Override + public List getTravelList(Long userId) { + + // ์—ฌํ–‰ ๋ฉค๋ฒ„ ํ…Œ์ด๋ธ”์—์„œ ์œ ์ € id๊ฐ€ ์†ํ•œ ์—ฌํ–‰ id ์กฐํšŒ + List travelMembers = travelMemberRepository.findAllByUserId(userId); + + return travelMembers.stream() + .map(travelMember -> { + Travel travel = travelMember.getTravel(); + + // ํŠน์ • ์—ฌํ–‰์— ์†ํ•œ ๋ชจ๋“  ๋ฉค๋ฒ„ ์กฐํšŒ + List members = travelMemberRepository.findAllByTravelId(travel.getId()); + + // ์บกํ‹ด ๋ฉค๋ฒ„ ํ•„ํ„ฐ๋ง ๋ฐ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์ถ”์ถœ + int profileImage = members.stream() + .filter(TravelMember::isCaptain) + .map(captain -> captain.getUser().getProfileImage()) + .findFirst() + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_CAPTAIN_NOT_FOUND)); + + // ํŠน์ • ์—ฌํ–‰์— ์†ํ•œ ๋ชจ๋“  ๊ตญ๊ฐ€ ์กฐํšŒ + List travelCountries = travelCountryRepository.findByTravelId(travel.getId()); + + // TravelListResponse ์ƒ์„ฑ + return TravelListResponse.from(travel, travelCountries, profileImage); + }).toList(); + + } + + + + // ์—ฐ๊ฒฐํ•œ ๊ณ„์ขŒ ๋ฐ ์œ„๋น„ ์นด๋“œ ๋ฐœ๊ธ‰ ํ–ˆ๋Š”์ง€ ์•ˆํ–ˆ๋Š” ์ง€ ์—ฌ๋ถ€ + @Override + public void postConnectedAccount(CardCompletedRequest request,Long userId){ + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND)); + // ์นด๋“œ ๋ฐœ๊ธ‰ x , ์—ฐ๊ฒฐ๊ณ„์ขŒ 0 + if(!request.isWibeeCard()){ + Account account = accountRepository.findById(request.getAccountId()) + .orElseThrow(() -> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + user.updateConnectedAccount(account); + } + + // ์นด๋“œ ๋ฐœ๊ธ‰ 0 , ์—ฐ๊ฒฐ๊ณ„์ขŒ 0 + if (request.isWibeeCard()) { + Account account = accountRepository.findById(request.getAccountId()) + .orElseThrow(() -> new CustomException(BankingErrorCode.ACCOUNT_NOT_FOUND)); + + account.updatedAccount(request.isWibeeCard()); + user.updateConnectedAccount(account); + user.updateWibeeCardAccount(account); + } + + } + + @Override + public AccountConnectedWibeeResponse getConnectedWibee(Long userId){ + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(AuthErrorCode.USER_NOT_FOUND)); + boolean isConnected = user.getWibeeCardAccount() != null; + return new AccountConnectedWibeeResponse(isConnected); + } + + @Override + public void changeMainImage(Long travelId, MultipartFile image) { + // ์—ฌํ–‰ ์ •๋ณด ์ฐพ์•„์˜ค๊ธฐ + Travel travel = travelRepository.findById(travelId) + .orElseThrow(() -> new CustomException(TravelErrorCode.TRAVEL_NOT_FOUND)); + + // ์ด๋ฏธ์ง€ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ + if(image != null && image.getSize() != 0) { + // ์ด๋ฏธ์ง€ ์ €์žฅํ•  S3 ๋””๋ ‰ํ† ๋ฆฌ ์ •๋ณด + String dirName = TRAVEL_IMAGE_DIR + travelId; + try { + String newImage = s3Uploader.upload(image, dirName); + travel.updateMainImage(newImage); + } catch (IOException e) { + throw new CustomException(ValidationErrorCode.IMAGE_PROCESSING_FAILED); + } + } + } +} diff --git a/src/test/java/withbeetravel/controller/settlement/SettlementControllerTest.java b/src/test/java/withbeetravel/controller/settlement/SettlementControllerTest.java new file mode 100644 index 00000000..73bb4dce --- /dev/null +++ b/src/test/java/withbeetravel/controller/settlement/SettlementControllerTest.java @@ -0,0 +1,64 @@ +package withbeetravel.controller.settlement; + +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import withbeetravel.support.BaseIntegrationTest; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpHeaders.AUTHORIZATION; + +class SettlementControllerTest extends BaseIntegrationTest { + + @Test + void ์ •์‚ฐ์„_์š”์ฒญํ•œ๋‹ค() { + given().log().all() + .header(AUTHORIZATION, "Bearer " + accessToken) + .contentType(ContentType.JSON) + .when() + .post("/api/travels/1/settlements") + .then() + .log() + .all() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void ์ •์‚ฐ์„_๋™์˜ํ•œ๋‹ค() { + given().log().all() + .header(AUTHORIZATION, "Bearer " + accessToken) + .contentType(ContentType.JSON) + .when() + .post("/api/travels/1/settlements/agreement") + .then() + .log() + .all() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void ์ •์‚ฐ์„_์ทจ์†Œํ•œ๋‹ค() { + given().log().all() + .header(AUTHORIZATION, "Bearer " + accessToken) + .contentType(ContentType.JSON) + .when() + .delete("/api/travels/1/settlements") + .then() + .log() + .all() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void ์„ธ๋ถ€์ง€์ถœ๋‚ด์—ญ์„_์กฐํšŒํ•œ๋‹ค() { + given().log().all() + .header(AUTHORIZATION, "Bearer " + accessToken) + .contentType(ContentType.JSON) + .when() + .get("/api/travels/1/settlements") + .then() + .log() + .all() + .statusCode(HttpStatus.OK.value()); + } +} diff --git a/src/test/java/withbeetravel/service/auth/AuthServiceTest.java b/src/test/java/withbeetravel/service/auth/AuthServiceTest.java new file mode 100644 index 00000000..d4ff4134 --- /dev/null +++ b/src/test/java/withbeetravel/service/auth/AuthServiceTest.java @@ -0,0 +1,90 @@ +package withbeetravel.service.auth; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import withbeetravel.domain.Account; +import withbeetravel.domain.User; +import withbeetravel.dto.request.auth.SignUpRequest; +import withbeetravel.dto.response.auth.MyPageResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.AuthErrorCode; +import withbeetravel.jwt.JwtUtil; +import withbeetravel.repository.LoginLogRepository; +import withbeetravel.repository.RefreshTokenRepository; +import withbeetravel.repository.UserRepository; +import withbeetravel.service.loginLog.LoginLogServiceImpl; +import withbeetravel.support.AccountFixture; +import withbeetravel.support.UserFixture; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @Mock private UserRepository userRepository; + @Mock private BCryptPasswordEncoder bCryptPasswordEncoder; + @Mock private JwtUtil jwtUtil; + @Mock private RefreshTokenRepository refreshTokenRepository; + @Mock private LoginLogRepository loginLogRepository; + @Mock private LoginLogServiceImpl loginLogService; + @InjectMocks private AuthServiceImpl authService; + + @Test + void ์‚ฌ์šฉ์ž_์ •๋ณด๋ฅผ_์ •์ƒ์ ์œผ๋กœ_๊ฐ€์ ธ์˜ฌ_์ˆ˜_์žˆ๋‹ค() { + User user = UserFixture.builder().id(1L).build(); + Account account = AccountFixture.builder().user(user).isConnectedWibeeCard(true).build(); + + user.updateConnectedAccount(account); + user.updateWibeeCardAccount(account); + + // given + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); + + // when + MyPageResponse response = authService.getMyPageInfo(user.getId()); + + // then + assertAll( + () -> assertNotNull(response), + () -> assertEquals(1, response.getProfileImage()), + () -> assertEquals("ํ™๊ธธ๋™", response.getUsername()), + () -> assertEquals("WONํ†ต์žฅ", response.getAccountProduct()), + () -> assertEquals("1111111111111", response.getAccountNumber()) + ); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์‚ฌ์šฉ์ž_ID์—_๋Œ€ํ•œ_์˜ˆ์™ธ๋ฅผ_๋ฐœ์ƒ์‹œํ‚จ๋‹ค() { + // given + Long userId = 1L; + given(userRepository.findById(userId)).willReturn(Optional.empty()); + + // when & then + CustomException exception = assertThrows(CustomException.class, () -> authService.getMyPageInfo(userId)); + assertEquals(AuthErrorCode.USER_NOT_FOUND, exception.getErrorCode()); + } + + @Test + void ํšŒ์›๊ฐ€์ž…์„_ํ• _์ˆ˜_์žˆ๋‹ค() { + SignUpRequest signUpRequest = new SignUpRequest( + "1234@naver.com", "password123!", "123456", "๊ณต์˜ˆ์ง„"); + + given(userRepository.existsByEmail(signUpRequest.getEmail())).willReturn(false); + + authService.signUp(signUpRequest); + verify(userRepository, times(1)).save(any(User.class)); + verify(loginLogService, times(1)) + .logRegister(any(User.class), eq(signUpRequest.getEmail())); + } +} \ No newline at end of file diff --git a/src/test/java/withbeetravel/service/payment/SharedPaymentParticipantServiceTest.java b/src/test/java/withbeetravel/service/payment/SharedPaymentParticipantServiceTest.java new file mode 100644 index 00000000..eb316330 --- /dev/null +++ b/src/test/java/withbeetravel/service/payment/SharedPaymentParticipantServiceTest.java @@ -0,0 +1,137 @@ +package withbeetravel.service.payment; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import withbeetravel.domain.*; +import withbeetravel.dto.request.payment.SharedPaymentParticipateRequest; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.PaymentErrorCode; +import withbeetravel.repository.PaymentParticipatedMemberRepository; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelMemberRepository; +import withbeetravel.support.SharedPaymentFixture; +import withbeetravel.support.TravelFixture; +import withbeetravel.support.UserFixture; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SharedPaymentParticipantServiceTest { + + @Mock private TravelMemberRepository travelMemberRepository; + @Mock private SharedPaymentRepository sharedPaymentRepository; + @Mock private PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + + @InjectMocks private SharedPaymentParticipantServiceImpl service; + + @Test + void ์ •์‚ฐ_์ฐธ์—ฌ_๋ฉค๋ฒ„๋ฅผ_์ •์ƒ์ ์œผ๋กœ_๋ณ€๊ฒฝํ• _์ˆ˜_์žˆ๋‹ค() { + + // Given + User user1 = UserFixture.builder().id(1L).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Long travelId = 1L; + Travel travel = TravelFixture.builder().id(travelId).build(); + + List travelMembers = Arrays.asList( + TravelMember.builder().id(1L).travel(travel).user(user1).isCaptain(true).build(), + TravelMember.builder().id(2L).travel(travel).user(user2).isCaptain(false).build(), + TravelMember.builder().id(3L).travel(travel).user(user3).isCaptain(false).build() + ); + + Long sharedPaymentId = 1L; + SharedPayment sharedPayment = SharedPaymentFixture.builder() + .id(sharedPaymentId) + .addedByMember(travelMembers.get(0)) + .travel(travel) + .participantCount(2) + .build(); + + List existingParticipants = Arrays.asList( + PaymentParticipatedMember.builder() + .id(1L) + .travelMember(travelMembers.get(0)) + .sharedPayment(sharedPayment) + .build(), + PaymentParticipatedMember.builder() + .id(2L) + .travelMember(travelMembers.get(1)) + .sharedPayment(sharedPayment) + .build() + ); + + List newParticipantIds = Arrays.asList(1L, 3L); + SharedPaymentParticipateRequest request = new SharedPaymentParticipateRequest(newParticipantIds); + + given(travelMemberRepository.findAllByTravelId(travelId)).willReturn(travelMembers); + given(sharedPaymentRepository.findById(sharedPaymentId)).willReturn(Optional.of(sharedPayment)); + given(paymentParticipatedMemberRepository.findAllBySharedPaymentId(sharedPaymentId)) + .willReturn(existingParticipants); + + // When + service.updateParticipantMembers(travelId, sharedPaymentId, request); + + // Then + assertEquals(2, sharedPayment.getParticipantCount(), "์ฐธ์—ฌ ๋ฉค๋ฒ„ ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); + + // ์ €์žฅ๋œ ์ฐธ์—ฌ ๋ฉค๋ฒ„ ๊ฒ€์ฆ + ArgumentCaptor captor = ArgumentCaptor.forClass(PaymentParticipatedMember.class); + verify(paymentParticipatedMemberRepository, times(1)).save(captor.capture()); + + List savedMembers = captor.getAllValues(); + List savedMemberIds = savedMembers.stream() + .map(member -> member.getTravelMember().getId()) + .toList(); + + assertEquals(1, savedMemberIds.size(), "์ฐธ์—ฌ ๋ฉค๋ฒ„์— ์ƒˆ๋กœ ์ €์žฅ๋œ ์ˆ˜๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + assertTrue(savedMemberIds.containsAll(Arrays.asList(3L)), "์ƒˆ๋กœ ์ €์žฅ๋œ ๋ฉค๋ฒ„๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค."); + + // ์‚ญ์ œ๋œ ์ฐธ์—ฌ ๋ฉค๋ฒ„ ๊ฒ€์ฆ + verify(paymentParticipatedMemberRepository, times(1)).delete(any(PaymentParticipatedMember.class)); + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(PaymentParticipatedMember.class); + verify(paymentParticipatedMemberRepository).delete(deleteCaptor.capture()); + assertEquals(2L, deleteCaptor.getValue().getTravelMember().getId(), "์‚ญ์ œ๋œ ๋ฉค๋ฒ„๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค."); + } + + @Test + void ์—ฌํ–‰_๋ฉค๋ฒ„๊ฐ€_์•„๋‹Œ_์‚ฌ์šฉ์ž๊ฐ€_ํฌํ•จ๋˜์–ด_์žˆ์„_๊ฒฝ์šฐ_์˜ˆ์™ธ๋ฅผ_๋ฐœ์ƒ์‹œํ‚จ๋‹ค() { + // Given + User user1 = UserFixture.builder().id(1L).build(); + User user2 = UserFixture.builder().id(2L).build(); + + Long travelId = 1L; + Travel travel = TravelFixture.builder().id(travelId).build(); + + List travelMembers = Arrays.asList( + TravelMember.builder().id(1L).travel(travel).user(user1).isCaptain(true).build(), + TravelMember.builder().id(2L).travel(travel).user(user2).isCaptain(false).build() + ); + + List invalidParticipantIds = Arrays.asList(1L, 3L); // 3L์€ ์—ฌํ–‰ ๋ฉค๋ฒ„์— ํฌํ•จ๋˜์ง€ ์•Š์Œ + SharedPaymentParticipateRequest request = new SharedPaymentParticipateRequest(invalidParticipantIds); + + given(travelMemberRepository.findAllByTravelId(travelId)).willReturn(travelMembers); + + // When & Then + CustomException exception = assertThrows( + CustomException.class, + () -> service.updateParticipantMembers(travelId, 1L, request) + ); + + assertEquals(PaymentErrorCode.NON_TRAVEL_MEMBER_INCLUDED, exception.getErrorCode()); + } +} \ No newline at end of file diff --git a/src/test/java/withbeetravel/service/payment/SharedPaymentRecordServiceTest.java b/src/test/java/withbeetravel/service/payment/SharedPaymentRecordServiceTest.java new file mode 100644 index 00000000..248b2f01 --- /dev/null +++ b/src/test/java/withbeetravel/service/payment/SharedPaymentRecordServiceTest.java @@ -0,0 +1,201 @@ +package withbeetravel.service.payment; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.domain.SharedPayment; +import withbeetravel.domain.Travel; +import withbeetravel.dto.response.payment.SharedPaymentRecordResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.ValidationErrorCode; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelRepository; +import withbeetravel.service.global.S3Uploader; +import withbeetravel.support.SharedPaymentFixture; +import withbeetravel.support.TravelFixture; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SharedPaymentRecordServiceTest { + + @Mock private S3Uploader s3Uploader; + @Mock private TravelRepository travelRepository; + @Mock private SharedPaymentRepository sharedPaymentRepository; + @Mock private MultipartFile image; + + @InjectMocks private SharedPaymentRecordServiceImpl service; + + @Test + void ์ด๋ฏธ์ง€_์—…๋กœ๋“œ_๋ฐ_๋ฉ”์ธ_์ด๋ฏธ์ง€_์ˆ˜์ •_์„ฑ๊ณต() throws IOException { + // Given + Long travelId = 1L; + Long sharedPaymentId = 1L; + String newImageUrl = "https://s3.amazonaws.com/shared-payments/travel-1/image.png"; + String comment = "ํ…Œ์ŠคํŠธ"; + boolean isMainImage = true; + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + SharedPayment sharedPayment = SharedPaymentFixture.builder() + .id(sharedPaymentId) + .travel(travel) + .build(); + + given(sharedPaymentRepository.findById(sharedPaymentId)).willReturn(Optional.of(sharedPayment)); + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + + given(image.getSize()).willReturn(100L); + + // Mocking the upload method + given(s3Uploader.upload(image, "shared-payments/" + travelId)).willReturn(newImageUrl); + + // When + service.addAndUpdatePaymentRecord(travelId, sharedPaymentId, image, comment, isMainImage); + + // Then + verify(sharedPaymentRepository).findById(sharedPaymentId); + verify(travelRepository).findById(travelId); + verify(s3Uploader).upload(image, "shared-payments/" + travelId); + assertEquals(newImageUrl, sharedPayment.getPaymentImage()); + assertEquals(newImageUrl, travel.getMainImage()); + assertEquals(comment, sharedPayment.getPaymentComment()); + } + + @Test + void ๊ธฐ์กด_์ด๋ฏธ์ง€๋ฅผ_๋ฉ”์ธ_์ด๋ฏธ์ง€๋กœ_์„ค์ •_์„ฑ๊ณต() { + // Given + Long travelId = 1L; + Long sharedPaymentId = 1L; + String existingImageUrl = "https://s3.amazonaws.com/shared-payments/travel-1/old-image.png"; + String comment = "Updated payment comment"; + boolean isMainImage = true; + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + SharedPayment sharedPayment = SharedPaymentFixture.builder() + .id(sharedPaymentId) + .travel(travel) + .paymentImage(existingImageUrl) + .build(); + + given(sharedPaymentRepository.findById(sharedPaymentId)).willReturn(Optional.of(sharedPayment)); + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + + // When + service.addAndUpdatePaymentRecord(travelId, sharedPaymentId, null, comment, isMainImage); + + // Then + verify(sharedPaymentRepository).findById(sharedPaymentId); + verify(travelRepository).findById(travelId); + assertEquals(existingImageUrl, travel.getMainImage()); + assertEquals(comment, sharedPayment.getPaymentComment()); + } + + @Test + void ์ด๋ฏธ์ง€_์—…๋กœ๋“œ_์‹คํŒจ์‹œ_์˜ˆ์™ธ_๋ฐœ์ƒ() throws IOException { + // Given + Long travelId = 1L; + Long sharedPaymentId = 1L; + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + SharedPayment sharedPayment = SharedPaymentFixture.builder() + .id(sharedPaymentId) + .travel(travel) + .build(); + + given(sharedPaymentRepository.findById(sharedPaymentId)).willReturn(Optional.of(sharedPayment)); + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(image.getSize()).willReturn(100L); + given(s3Uploader.upload(image, "shared-payments/" + travelId)).willThrow(IOException.class); + + // When & Then + CustomException exception = assertThrows( + CustomException.class, + () -> service.addAndUpdatePaymentRecord(travelId, sharedPaymentId, image, "Comment", false) + ); + + assertEquals(ValidationErrorCode.IMAGE_PROCESSING_FAILED, exception.getErrorCode()); + verify(sharedPaymentRepository).findById(sharedPaymentId); + verify(travelRepository).findById(travelId); + } + + @Test + void ๋ฉ”์ธ_์ด๋ฏธ์ง€๋กœ_์‚ฌ์šฉํ•˜๋Š”_์ด๋ฏธ์ง€๋ฅผ_ํฌํ•จํ•œ_์—ฌํ–‰_๊ธฐ๋ก_๊ฐ€์ ธ์˜ค๊ธฐ () { + // Given + Long sharedPaymentId = 1L; + Long travelId = 1L; + String imageUrl = "imageUrl"; + + Travel travel = TravelFixture.builder() + .id(travelId) + .mainImage(imageUrl) + .build(); + + SharedPayment sharedPayment = SharedPaymentFixture.builder() + .id(sharedPaymentId) + .travel(travel) + .paymentImage(imageUrl) + .build(); + + // Mocking Repository์—์„œ ์—”ํ‹ฐํ‹ฐ ๋ฐ˜ํ™˜ + given(sharedPaymentRepository.findById(sharedPaymentId)).willReturn(Optional.of(sharedPayment)); + + // When + SharedPaymentRecordResponse response = service.getSharedPaymentRecord(sharedPaymentId); + + // Then + assertNotNull(response); + assertEquals("imageUrl", response.getPaymentImage()); + assertTrue(response.isMainImage()); + + verify(sharedPaymentRepository).findById(sharedPaymentId); + } + + @Test + void ๋ฉ”์ธ_์ด๋ฏธ์ง€๋กœ_์‚ฌ์šฉํ•˜์ง€_์•Š๋Š”_์ด๋ฏธ์ง€๋ฅผ_ํฌํ•จํ•œ_์—ฌํ–‰_๊ธฐ๋ก_๊ฐ€์ ธ์˜ค๊ธฐ () { + // Given + Long sharedPaymentId = 1L; + Long travelId = 1L; + String imageUrl = "imageUrl"; + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + SharedPayment sharedPayment = SharedPaymentFixture.builder() + .id(sharedPaymentId) + .travel(travel) + .paymentImage(imageUrl) + .build(); + + // Mocking Repository์—์„œ ์—”ํ‹ฐํ‹ฐ ๋ฐ˜ํ™˜ + given(sharedPaymentRepository.findById(sharedPaymentId)).willReturn(Optional.of(sharedPayment)); + + // When + SharedPaymentRecordResponse response = service.getSharedPaymentRecord(sharedPaymentId); + + // Then + assertNotNull(response); + assertEquals("imageUrl", response.getPaymentImage()); + assertFalse(response.isMainImage()); + + // Repository ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ ๊ฒƒ์„ ๊ฒ€์ฆ + verify(sharedPaymentRepository).findById(sharedPaymentId); + } +} \ No newline at end of file diff --git a/src/test/java/withbeetravel/service/payment/SharedPaymentRegisterServiceTest.java b/src/test/java/withbeetravel/service/payment/SharedPaymentRegisterServiceTest.java new file mode 100644 index 00000000..1bd80bd2 --- /dev/null +++ b/src/test/java/withbeetravel/service/payment/SharedPaymentRegisterServiceTest.java @@ -0,0 +1,481 @@ +package withbeetravel.service.payment; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.multipart.MultipartFile; +import withbeetravel.domain.*; +import withbeetravel.exception.CustomException; +import withbeetravel.exception.error.ValidationErrorCode; +import withbeetravel.repository.*; +import withbeetravel.service.global.S3Uploader; +import withbeetravel.support.TravelFixture; +import withbeetravel.support.UserFixture; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SharedPaymentRegisterServiceTest { + + @Mock private TravelRepository travelRepository; + @Mock private TravelMemberRepository travelMemberRepository; + @Mock private SharedPaymentRepository sharedPaymentRepository; + @Mock private PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + @Mock private UserRepository userRepository; + @Mock private HistoryRepository historyRepository; + @Mock private TravelCountryRepository travelCountryRepository; + @Mock private SharedPaymentCategoryClassificationService sharedPaymentCategoryClassificationService; + @Mock private S3Uploader s3Uploader; + @Mock private MultipartFile paymentImage; + @Mock private MultipartFile image; + + @InjectMocks private SharedPaymentRegisterServiceImpl sharedPaymentRegisterService; + + @Test + void ์›ํ™”_๊ฒฐ์ œ_๋‚ด์—ญ์ด_์„ฑ๊ณต์ ์œผ๋กœ_์ถ”๊ฐ€๋œ๋‹ค() { + // Given + Long userId = 1L; + Long travelId = 1L; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + given(travelMemberRepository.findAllByTravelId(travelId)).willReturn(travelMembers); + + // When + sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06 20:04", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 15000, + null, + "KRW", + null, + null, + null, + false + ); + + // Then + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + verify(travelMemberRepository).findAllByTravelId(travelId); + verify(sharedPaymentRepository).save(any(SharedPayment.class)); + verify(paymentParticipatedMemberRepository, times(3)).save(any(PaymentParticipatedMember.class)); + } + + @Test + void ์™ธํ™”_๊ฒฐ์ œ_๋‚ด์—ญ์ด_์„ฑ๊ณต์ ์œผ๋กœ_์ถ”๊ฐ€๋œ๋‹ค() { + // Given + Long userId = 1L; + Long travelId = 1L; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + given(travelMemberRepository.findAllByTravelId(travelId)).willReturn(travelMembers); + + // When + sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06 20:04", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 14000, + 10.0, + "USD", + 1400.0, + null, + null, + false + ); + + // Then + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + verify(travelMemberRepository).findAllByTravelId(travelId); + verify(sharedPaymentRepository).save(any(SharedPayment.class)); + verify(paymentParticipatedMemberRepository, times(3)).save(any(PaymentParticipatedMember.class)); + } + + @Test + void ์™ธํ™”_๊ฒฐ์ œ_๋‚ด์—ญ_์ถ”๊ฐ€์—_์‹คํŒจํ•œ๋‹ค() { + // Given + Long userId = 1L; + Long travelId = 1L; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + + // When & Then + CustomException exception = assertThrows( + CustomException.class, + () -> sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06 20:04", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 14000, + 10.0, + "USD", + null, + null, + null, + false + ) + ); + + assertEquals(ValidationErrorCode.MISSING_REQUIRED_FIELDS, exception.getErrorCode()); + + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + } + + @Test + void ์‚ฌ์ง„์„_ํฌํ•จํ•œ_๊ฒฐ์ œ_๋‚ด์—ญ์ด_์„ฑ๊ณต์ ์œผ๋กœ_์ถ”๊ฐ€๋œ๋‹ค() throws IOException { + // Given + Long userId = 1L; + Long travelId = 1L; + String newImageUrl = "https://s3.amazonaws.com/shared-payments/travel-1/image.png"; + String comment = "ํ…Œ์ŠคํŠธ"; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + given(image.getSize()).willReturn(100L); + given(s3Uploader.upload(image, "shared-payments/" + travelId)).willReturn(newImageUrl); + given(travelMemberRepository.findAllByTravelId(travelId)).willReturn(travelMembers); + + // When + sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06 20:04", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 15000, + null, + "KRW", + null, + image, + comment, + false + ); + + // Then + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + verify(s3Uploader).upload(image, "shared-payments/" + travelId); + verify(travelMemberRepository).findAllByTravelId(travelId); + verify(sharedPaymentRepository).save(any(SharedPayment.class)); + verify(paymentParticipatedMemberRepository, times(3)).save(any(PaymentParticipatedMember.class)); + } + + @Test + void ์‚ฌ์ง„์„_ํฌํ•จํ•œ_๊ฒฐ์ œ_๋‚ด์—ญ_์ด๋ฐŽ_์—…๋กœ๋“œ_์‹คํŒจ_์‹œ_์˜ˆ์™ธ_๋ฐœ์ƒ() throws IOException { + // Given + Long userId = 1L; + Long travelId = 1L; + String newImageUrl = "https://s3.amazonaws.com/shared-payments/travel-1/image.png"; + String comment = "ํ…Œ์ŠคํŠธ"; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + given(image.getSize()).willReturn(100L); + given(s3Uploader.upload(image, "shared-payments/" + travelId)).willThrow(IOException.class); + + // When & Then + CustomException exception = assertThrows( + CustomException.class, + () -> sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06 20:04", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 15000, + null, + "KRW", + null, + image, + comment, + false + ) + ); + + assertEquals(ValidationErrorCode.IMAGE_PROCESSING_FAILED, exception.getErrorCode()); + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + verify(s3Uploader).upload(image, "shared-payments/" + travelId); + } + + @Test + void ์œ ํšจํ•˜์ง€_์•Š์€_๋‚ ์งœ_ํ˜•์‹_์ž…๋ ฅ_์‹œ_์˜ˆ์™ธ_๋ฐœ์ƒ() { + // Given + Long userId = 1L; + Long travelId = 1L; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + + // When & Then + CustomException exception = assertThrows( + CustomException.class, + () -> sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 14000, + 10.0, + "USD", + 1400.0, + null, + null, + false + ) + ); + + assertEquals(ValidationErrorCode.INVALID_DATE_FORMAT, exception.getErrorCode()); + + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + verify(travelMemberRepository).findAllByTravelId(travelId); + } + + @Test + void ์ง€์›๋˜์ง€_์•Š๋Š”_ํ†ตํ™”_์ฝ”๋“œ_์ž…๋ ฅ_์‹œ_์˜ˆ์™ธ_๋ฐœ์ƒ() { + // Given + Long userId = 1L; + Long travelId = 1L; + + User user1 = UserFixture.builder().id(userId).build(); + User user2 = UserFixture.builder().id(2L).build(); + User user3 = UserFixture.builder().id(3L).build(); + + Travel travel = TravelFixture.builder() + .id(travelId) + .build(); + + TravelMember travelMember1 = TravelMember.builder() + .id(1L) + .travel(travel) + .user(user1) + .isCaptain(true) + .build(); + TravelMember travelMember2 = TravelMember.builder() + .id(2L) + .travel(travel) + .user(user2) + .isCaptain(false) + .build(); + TravelMember travelMember3 = TravelMember.builder() + .id(3L) + .travel(travel) + .user(user3) + .isCaptain(false) + .build(); + List travelMembers = Arrays.asList(travelMember1, travelMember2, travelMember3); + + given(travelRepository.findById(travelId)).willReturn(Optional.of(travel)); + given(travelMemberRepository.findByTravelIdAndUserId(travelId, userId)).willReturn(Optional.of(travelMember1)); + + // When & Then + CustomException exception = assertThrows( + CustomException.class, + () -> sharedPaymentRegisterService.addManualSharedPayment( + userId, + travelId, + "2024-12-06 20:04", + "๋ช…์ˆ˜๋„ค ๋–ก๋ณถ์ด", + 14000, + 10.0, + "USS", + 1400.0, + null, + null, + false + ) + ); + + assertEquals(ValidationErrorCode.INVALID_CURRENCY_UNIT, exception.getErrorCode()); + + verify(travelRepository).findById(travelId); + verify(travelMemberRepository).findByTravelIdAndUserId(travelId, userId); + verify(travelMemberRepository).findAllByTravelId(travelId); + } +} \ No newline at end of file diff --git a/src/test/java/withbeetravel/service/payment/SharedPaymentServiceImplTest.java b/src/test/java/withbeetravel/service/payment/SharedPaymentServiceImplTest.java new file mode 100644 index 00000000..d80d534d --- /dev/null +++ b/src/test/java/withbeetravel/service/payment/SharedPaymentServiceImplTest.java @@ -0,0 +1,141 @@ +package withbeetravel.service.payment; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import withbeetravel.domain.Category; +import withbeetravel.domain.PaymentParticipatedMember; +import withbeetravel.domain.SharedPayment; +import withbeetravel.domain.TravelMember; +import withbeetravel.domain.User; +import withbeetravel.dto.request.payment.SharedPaymentSearchRequest; +import withbeetravel.dto.response.payment.SharedPaymentParticipatingMemberResponse; +import withbeetravel.exception.CustomException; +import withbeetravel.repository.SharedPaymentRepository; +import withbeetravel.repository.TravelMemberRepository; +import withbeetravel.support.UserFixture; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SharedPaymentServiceImplTest { + + @Mock + private SharedPaymentRepository sharedPaymentRepository; + + @Mock + private TravelMemberRepository travelMemberRepository; + + @InjectMocks + private SharedPaymentServiceImpl sharedPaymentService; + + @Test + void getSharedPayments_์ •์ƒ์กฐํšŒ() { + // given + Long travelId = 1L; + SharedPaymentSearchRequest request = new SharedPaymentSearchRequest(); + request.setPage(0); + request.setSortBy("latest"); + request.setStartDate(LocalDate.now().minusDays(7)); + request.setEndDate(LocalDate.now()); + request.setCategory("์ˆ™๋ฐ•"); + + SharedPayment payment = SharedPayment.builder() + .id(1L) + .paymentAmount(100000) + .category(Category.ACCOMMODATION) + .build(); + + Page mockPage = new PageImpl<>(List.of(payment)); + + when(sharedPaymentRepository.findAllByTravelIdAndMemberIdAndDateRange( + any(), any(), any(), any(), any(), any())) + .thenReturn(mockPage); + + // when + Page result = sharedPaymentService.getSharedPayments(travelId, request); + + // then + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(1, result.getTotalElements()); + assertEquals(Category.ACCOMMODATION, result.getContent().get(0).getCategory()); + } + + @Test + void getSharedPayments_๊ฒฐ๊ณผ์—†์Œ_์˜ˆ์™ธ๋ฐœ์ƒ() { + // given + Long travelId = 1L; + SharedPaymentSearchRequest request = new SharedPaymentSearchRequest(); + request.setPage(0); + request.setSortBy("latest"); + + when(sharedPaymentRepository.findAllByTravelIdAndMemberIdAndDateRange( + any(), any(), any(), any(), any(), any())) + .thenReturn(Page.empty()); + + // when & then + assertThrows(CustomException.class, + () -> sharedPaymentService.getSharedPayments(travelId, request)); + } + + @Test + void getParticipatingMembersMap_์ •์ƒ๋งคํ•‘() { + // given + User user = UserFixture.builder().id(1L).build(); + + TravelMember travelMember = TravelMember.builder() + .id(1L) + .user(user) + .build(); + + PaymentParticipatedMember participatedMember = PaymentParticipatedMember.builder() + .travelMember(travelMember) + .build(); + + SharedPayment payment = SharedPayment.builder() + .id(1L) + .build(); + + payment.addPaymentParticipatedMember(participatedMember); + + Page payments = new PageImpl<>(List.of(payment)); + + // when + Map> result = + sharedPaymentService.getParticipatingMembersMap(payments); + + // then + assertNotNull(result); + assertEquals(1, result.size()); + assertTrue(result.containsKey(1L)); + + List memberResponses = result.get(1L); + assertNotNull(memberResponses); + assertEquals(1, memberResponses.size()); + assertEquals(1L, memberResponses.get(0).getId()); + assertEquals(1, memberResponses.get(0).getProfileImage()); + } + + @Test + void getSharedPayments_์ž˜๋ชป๋œ์นดํ…Œ๊ณ ๋ฆฌ_์˜ˆ์™ธ๋ฐœ์ƒ() { + // given + Long travelId = 1L; + SharedPaymentSearchRequest request = new SharedPaymentSearchRequest(); + request.setCategory("invalid_category"); + + // when & then + assertThrows(CustomException.class, + () -> sharedPaymentService.getSharedPayments(travelId, request)); + } +} \ No newline at end of file diff --git a/src/test/java/withbeetravel/service/settlement/SettlementServiceTest.java b/src/test/java/withbeetravel/service/settlement/SettlementServiceTest.java new file mode 100644 index 00000000..b6315be0 --- /dev/null +++ b/src/test/java/withbeetravel/service/settlement/SettlementServiceTest.java @@ -0,0 +1,127 @@ +package withbeetravel.service.settlement; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.scheduling.TaskScheduler; +import withbeetravel.domain.*; +import withbeetravel.repository.*; +import withbeetravel.repository.notification.EmitterRepository; +import withbeetravel.support.*; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SettlementServiceTest { + + @Mock private TravelMemberRepository travelMemberRepository; + @Mock private SettlementRequestRepository settlementRequestRepository; + @Mock private TravelMemberSettlementHistoryRepository travelMemberSettlementHistoryRepository; + @Mock private PaymentParticipatedMemberRepository paymentParticipatedMemberRepository; + @Mock private UserRepository userRepository; + @Mock private TravelRepository travelRepository; + @Mock private SharedPaymentRepository sharedPaymentRepository; + @Mock private SettlementRequestLogRepository settlementRequestLogRepository; + @Mock private EmitterRepository emitterRepository; + @InjectMocks private SettlementServiceImpl settlementService; + @Mock private TaskScheduler taskScheduler; + + + @Test + void ์ •์‚ฐ_์š”์ฒญ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { + + TestData testData = SettlementTestFixture.createTestData(); + + given(travelRepository.findById(testData.travel.getId())).willReturn(Optional.of(testData.travel)); + given(travelMemberRepository.findByTravelIdAndUserId(testData.travel.getId(), testData.user1.getId())) + .willReturn(Optional.of(testData.travelMember1)); + given(travelMemberRepository.findAllByTravelId(testData.travel.getId())) + .willReturn(List.of(testData.travelMember1, testData.travelMember2)); + + given(settlementRequestRepository.save(any(SettlementRequest.class))).willReturn(testData.settlementRequest); + + given(travelMemberSettlementHistoryRepository.save(any(TravelMemberSettlementHistory.class))).willReturn(testData.settlementHistory2); + + given(paymentParticipatedMemberRepository.findAllByTravelMemberId(testData.travelMember1.getId())).willReturn(List.of(testData.pppm1, testData.pppm2)); + given(paymentParticipatedMemberRepository.findAllByTravelMemberId(testData.travelMember2.getId())).willReturn(List.of(testData.pppm3, testData.pppm4)); + + given(settlementRequestLogRepository.save(any(SettlementRequestLog.class))).willReturn(SettlementRequestLog.builder().user(testData.user1).travel(testData.travel).build()); + + // ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ + assertAll( + () -> settlementService.requestSettlement(testData.user1.getId(), testData.travel.getId()), + () -> verify(travelMemberRepository, times(1)) + .findByTravelIdAndUserId(testData.travel.getId(), testData.user1.getId()), + () -> verify(travelMemberRepository, times(2)) + .findAllByTravelId(testData.travel.getId()), + () -> verify(travelMemberRepository, times(2)).findAllByTravelId(testData.travel.getId()), + () -> verify(settlementRequestRepository, times(1)) + .save(any(SettlementRequest.class)), + () -> verify(travelMemberSettlementHistoryRepository, times(2)) + .save(any(TravelMemberSettlementHistory.class)), + () -> verify(settlementRequestLogRepository, times(2)).save(any(SettlementRequestLog.class)), + () -> verify(taskScheduler, times(1)).schedule(any(Runnable.class), any(Instant.class))); + } + + @Test + void ์ •์‚ฐ_๋™์˜๋ฅผ_ํ• _์ˆ˜_์žˆ๋‹ค() { + TestData testData = SettlementTestFixture.createTestData(); + testData.travel.updateSettlementStatus(SettlementStatus.ONGOING); + + given(travelRepository.findById(testData.travel.getId())).willReturn(Optional.of(testData.travel)); + given(travelMemberRepository.findByTravelIdAndUserId(testData.travel.getId(), testData.user1.getId())) + .willReturn(Optional.of(testData.travelMember1)); + + given(settlementRequestRepository.findByTravelId(testData.travel.getId())).willReturn(Optional.ofNullable(testData.settlementRequest)); + + given(travelMemberSettlementHistoryRepository + .findTravelMemberSettlementHistoryBySettlementRequestIdAndTravelMemberId( + testData.settlementRequest.getId(), testData.travelMember1.getId())) + .willReturn(testData.settlementHistory1); + + given(userRepository.findById(testData.user1.getId())).willReturn(Optional.of(testData.user1)); + + assertAll( + () -> settlementService.agreeSettlement(testData.user1.getId(), testData.travel.getId()), + () -> verify(travelMemberRepository, times(1)).findByTravelIdAndUserId(testData.user1.getId(), testData.travel.getId()), + () -> verify(settlementRequestRepository, times(1)).findByTravelId(testData.travel.getId()), + () -> verify(travelRepository, times(1)).findById(testData.travel.getId()), + () -> verify(travelMemberSettlementHistoryRepository, times(1)) + .findTravelMemberSettlementHistoryBySettlementRequestIdAndTravelMemberId( + testData.settlementRequest.getId(), testData.travelMember1.getId()) + ); + + } + + @Test + void ์ •์‚ฐ_์ทจ์†Œ๋ฅผ_ํ• _์ˆ˜_์žˆ๋‹ค() { + TestData testData = SettlementTestFixture.createTestData(); + testData.travel.updateSettlementStatus(SettlementStatus.ONGOING); + + given(travelRepository.findById(testData.travel.getId())).willReturn(Optional.of(testData.travel)); + + given(settlementRequestRepository.findByTravelId(testData.travel.getId())).willReturn(Optional.ofNullable(testData.settlementRequest)); + + given(travelMemberRepository.findAllByTravelId(testData.travel.getId())).willReturn(List.of(testData.travelMember1, testData.travelMember2)); + + given(settlementRequestLogRepository.save(any(SettlementRequestLog.class))) + .willReturn(SettlementRequestLog.builder().user(testData.user2).travel(testData.travel).build()); + + assertAll( + () -> settlementService.cancelSettlement(testData.user1.getId(), testData.travel.getId()), + () -> verify(settlementRequestRepository, times(1)).findByTravelId(testData.travel.getId()), + () -> verify(travelRepository, times(1)).findById(testData.travel.getId()), + () -> verify(travelMemberRepository, times(2)).findAllByTravelId(testData.travel.getId()), + () -> verify(settlementRequestRepository, times(1)).deleteById(testData.settlementRequest.getId()), + () -> verify(settlementRequestLogRepository, times(2)).save(any(SettlementRequestLog.class))); + } + } \ No newline at end of file diff --git a/src/test/java/withbeetravel/service/travel/TravelServiceImplTest.java b/src/test/java/withbeetravel/service/travel/TravelServiceImplTest.java new file mode 100644 index 00000000..8e6082cc --- /dev/null +++ b/src/test/java/withbeetravel/service/travel/TravelServiceImplTest.java @@ -0,0 +1,292 @@ +package withbeetravel.service.travel; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +import withbeetravel.domain.*; +import withbeetravel.dto.request.travel.TravelRequest; +import withbeetravel.dto.response.travel.TravelResponse; +import withbeetravel.repository.*; +import withbeetravel.exception.CustomException; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +public class TravelServiceImplTest { + + @Mock + private TravelRepository travelRepository; + + @Mock + private TravelCountryRepository travelCountryRepository; + + @Mock + private AccountRepository accountRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private TravelMemberRepository travelMemberRepository; + + @InjectMocks + private TravelServiceImpl travelService; + + private User mockUser; + private Account mockAccount; + + @BeforeEach + void setUp() { + mockUser = User.builder() + .id(1L) + .password("password123!") // ์•”ํ˜ธ๋ฅผ ๋ฐ˜๋“œ์‹œ ์„ค์ • + .name("Test User") + .email("test@example.com") + .pinNumber("1234") // ํ•€ ๋ฒˆํ˜ธ ์ถ”๊ฐ€ + .build(); + + mockAccount = Account.builder() + .isConnectedWibeeCard(true) + .user(mockUser) + .build(); + } + + @Test + void saveTravel_๊ตญ๋‚ด์—ฌํ–‰_์„ฑ๊ณต() { + // Given + TravelRequest request = new TravelRequest( + "์„œ์šธ ์—ฌํ–‰", + true, + null, + "2024-07-01", + "2024-07-05" + ); + + // Mock ์„ค์ • + when(accountRepository.findByUserId(anyLong())) + .thenReturn(List.of(mockAccount)); + + when(userRepository.findById(anyLong())) + .thenReturn(Optional.of(mockUser)); + + Travel savedTravel = Travel.builder() + .id(1L) + .travelName(request.getTravelName()) + .travelStartDate(LocalDate.parse(request.getTravelStartDate())) + .travelEndDate(LocalDate.parse(request.getTravelEndDate())) + .isDomesticTravel(request.isDomesticTravel()) + .build(); + + when(travelRepository.save(any(Travel.class))) + .thenReturn(savedTravel); + + // When + TravelResponse response = travelService.saveTravel(request, 1L); + + // Then + assertNotNull(response); + assertEquals("์„œ์šธ ์—ฌํ–‰", response.getName()); + assertEquals(LocalDate.parse("2024-07-01").toString(), response.getStartDate()); + assertEquals(LocalDate.parse("2024-07-05").toString(), response.getEndDate()); + + // Verify method calls + verify(accountRepository).findByUserId(1L); + verify(travelRepository).save(any(Travel.class)); + verify(travelMemberRepository).save(any(TravelMember.class)); + } + + @Test + void saveTravel_ํ•ด์™ธ์—ฌํ–‰_์„ฑ๊ณต() { + // Given + TravelRequest request = new TravelRequest( + "์œ ๋Ÿฝ ์—ฌํ–‰", + false, + List.of("ํ”„๋ž‘์Šค", "๋…์ผ"), // ISO ์ฝ”๋“œ์™€ ์ด๋ฆ„ ํ˜ผํ•ฉ + "2024-08-01", + "2024-08-15" + ); + + // Mock ์„ค์ • + when(accountRepository.findByUserId(anyLong())) + .thenReturn(List.of(mockAccount)); + + when(userRepository.findById(anyLong())) + .thenReturn(Optional.of(mockUser)); + + Travel savedTravel = Travel.builder() + .id(1L) + .travelName(request.getTravelName()) + .travelStartDate(LocalDate.parse(request.getTravelStartDate())) + .travelEndDate(LocalDate.parse(request.getTravelEndDate())) + .isDomesticTravel(request.isDomesticTravel()) + .build(); + + when(travelRepository.save(any(Travel.class))) + .thenReturn(savedTravel); + + // ๊ตญ๊ฐ€ ๋ชจํ‚น + List travelCountries = request.getTravelCountries().stream() + .map(countryName -> TravelCountry.builder() + .country(Country.findByName(countryName)) + .travel(savedTravel) + .build()) + .toList(); + + // When + TravelResponse response = travelService.saveTravel(request, 1L); + + // Then + assertNotNull(response); + assertEquals("์œ ๋Ÿฝ ์—ฌํ–‰", response.getName()); + assertEquals(LocalDate.parse("2024-08-01").toString(), response.getStartDate()); + assertEquals(LocalDate.parse("2024-08-15").toString(), response.getEndDate()); + assertEquals(List.of("FR", "DE"), response.getCountry()); + + // Verify method calls + verify(accountRepository).findByUserId(1L); + verify(travelRepository).save(any(Travel.class)); + verify(travelMemberRepository).save(any(TravelMember.class)); + verify(travelCountryRepository).saveAll(anyList()); + } + + @Test + void saveTravel_์œ„๋น„์นด๋“œ๋ฏธ์—ฐ๊ฒฐ_์˜ˆ์™ธ๋ฐœ์ƒ() { + // Given + TravelRequest request = new TravelRequest( + "์ œ์ฃผ๋„ ์—ฌํ–‰", + true, + null, + "2024-09-01", + "2024-09-05" + ); + + // Mock ์„ค์ • - ์œ„๋น„ ์นด๋“œ ๋ฏธ์—ฐ๊ฒฐ ์ƒํƒœ + Account unconnectedAccount = Account.builder() + .isConnectedWibeeCard(false) + .user(mockUser) + .build(); + + when(accountRepository.findByUserId(anyLong())) + .thenReturn(List.of(unconnectedAccount)); + + // When & Then + assertThrows(CustomException.class, + () -> travelService.saveTravel(request, 1L), + "์œ„๋น„ ์นด๋“œ ๋ฏธ์—ฐ๊ฒฐ ์‹œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + + verify(accountRepository).findByUserId(1L); + verify(travelRepository, never()).save(any(Travel.class)); + } + + @Test + void editTravel_๊ตญ๋‚ด์—ฌํ–‰_์„ฑ๊ณต() { + // Given + Long travelId = 1L; + TravelRequest request = new TravelRequest( + "์ˆ˜์ •๋œ ์—ฌํ–‰", + true, + null, + "2024-10-01", + "2024-10-10" + ); + + Travel existingTravel = Travel.builder() + .id(travelId) + .travelName("๊ธฐ์กด ์—ฌํ–‰") + .travelStartDate(LocalDate.parse("2024-09-01")) + .travelEndDate(LocalDate.parse("2024-09-10")) + .isDomesticTravel(true) + .build(); + + // Mock ์„ค์ • + when(travelRepository.findById(travelId)) + .thenReturn(Optional.of(existingTravel)); + + // When + travelService.editTravel(request, travelId); + + // Then + verify(travelRepository).findById(travelId); + verify(travelCountryRepository).deleteByTravel(existingTravel); + + // ์—ฌํ–‰ ์ •๋ณด๊ฐ€ ์ •ํ™•ํžˆ ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + assertEquals("์ˆ˜์ •๋œ ์—ฌํ–‰", existingTravel.getTravelName()); + assertEquals(LocalDate.parse("2024-10-01"), existingTravel.getTravelStartDate()); + assertEquals(LocalDate.parse("2024-10-10"), existingTravel.getTravelEndDate()); + } + + @Test + void editTravel_ํ•ด์™ธ์—ฌํ–‰_์„ฑ๊ณต() { + // Given + Long travelId = 1L; + TravelRequest request = new TravelRequest( + "์ˆ˜์ •๋œ ํ•ด์™ธ ์—ฌํ–‰", + false, + List.of("์ดํƒˆ๋ฆฌ์•„", "์ŠคํŽ˜์ธ"), + "2024-11-01", + "2024-11-15" + ); + + Travel existingTravel = Travel.builder() + .id(travelId) + .travelName("๊ธฐ์กด ํ•ด์™ธ ์—ฌํ–‰") + .travelStartDate(LocalDate.parse("2024-09-01")) + .travelEndDate(LocalDate.parse("2024-09-10")) + .isDomesticTravel(true) + .build(); + + // Mock ์„ค์ • + when(travelRepository.findById(travelId)) + .thenReturn(Optional.of(existingTravel)); + + // When + travelService.editTravel(request, travelId); + + // Then + verify(travelRepository).findById(travelId); + verify(travelCountryRepository).deleteByTravel(existingTravel); + verify(travelCountryRepository).saveAll(anyList()); + + // ์—ฌํ–‰ ์ •๋ณด๊ฐ€ ์ •ํ™•ํžˆ ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + assertEquals("์ˆ˜์ •๋œ ํ•ด์™ธ ์—ฌํ–‰", existingTravel.getTravelName()); + assertEquals(LocalDate.parse("2024-11-01"), existingTravel.getTravelStartDate()); + assertEquals(LocalDate.parse("2024-11-15"), existingTravel.getTravelEndDate()); + assertFalse(existingTravel.isDomesticTravel()); + } + + @Test + void editTravel_์—ฌํ–‰_๋ฏธ์กด์žฌ_์˜ˆ์™ธ๋ฐœ์ƒ() { + // Given + Long travelId = 999L; + TravelRequest request = new TravelRequest( + "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰", + true, + null, + "2024-12-01", + "2024-12-10" + ); + + // Mock ์„ค์ • + when(travelRepository.findById(travelId)) + .thenReturn(Optional.empty()); + + // When & Then + assertThrows(IllegalArgumentException.class, + () -> travelService.editTravel(request, travelId), + "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ฌํ–‰ ID๋กœ ์ˆ˜์ • ์‹œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + + verify(travelRepository).findById(travelId); + verify(travelCountryRepository, never()).deleteByTravel(any()); + } +} \ No newline at end of file diff --git a/src/test/java/withbeetravel/support/AccountFixture.java b/src/test/java/withbeetravel/support/AccountFixture.java new file mode 100644 index 00000000..f8726645 --- /dev/null +++ b/src/test/java/withbeetravel/support/AccountFixture.java @@ -0,0 +1,61 @@ +package withbeetravel.support; + +import withbeetravel.domain.Account; +import withbeetravel.domain.Product; +import withbeetravel.domain.User; + +public class AccountFixture { + + private Long id; + private User user; + private String accountNumber = "1111111111111"; + private long balance = 1000000L; + private Product product = Product.WONํ†ต์žฅ; + private boolean isConnectedWibeeCard = false; + + public static AccountFixture builder() { + return new AccountFixture(); + } + + public AccountFixture id(Long id) { + this.id = id; + return this; + } + + public AccountFixture user(User user) { + this.user = user; + return this; + } + + public AccountFixture accountNumber(String accountNumber) { + this.accountNumber = accountNumber; + return this; + } + + public AccountFixture balance(long balance) { + this.balance = balance; + return this; + } + + public AccountFixture product(Product product) { + this.product = product; + return this; + } + + public AccountFixture isConnectedWibeeCard(boolean isConnectedWibeeCard) { + this.isConnectedWibeeCard = isConnectedWibeeCard; + return this; + } + + public Account build() { + return Account.builder() + .id(id) + .user(user) + .accountNumber(accountNumber) + .balance(balance) + .product(product) + .isConnectedWibeeCard(isConnectedWibeeCard) + .build(); + } +} + diff --git a/src/test/java/withbeetravel/support/BaseIntegrationTest.java b/src/test/java/withbeetravel/support/BaseIntegrationTest.java new file mode 100644 index 00000000..e0abcae2 --- /dev/null +++ b/src/test/java/withbeetravel/support/BaseIntegrationTest.java @@ -0,0 +1,32 @@ +package withbeetravel.support; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import withbeetravel.jwt.JwtUtil; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BaseIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private JwtUtil jwtUtil; + + protected String accessToken; + + @BeforeEach + void setUp() { + accessToken = jwtUtil.generateAccessToken(String.valueOf(2L)); + } + + @BeforeEach + void setPort() { + // ๋žœ๋ค์œผ๋กœ ๋ฐฐ์ •๋ฐ›์€ ํฌํŠธ ์„ค์ • + RestAssured.port = port; + } +} + diff --git a/src/test/java/withbeetravel/support/SettlementRequestFixture.java b/src/test/java/withbeetravel/support/SettlementRequestFixture.java new file mode 100644 index 00000000..25aef391 --- /dev/null +++ b/src/test/java/withbeetravel/support/SettlementRequestFixture.java @@ -0,0 +1,54 @@ +package withbeetravel.support; + +import withbeetravel.domain.SettlementRequest; +import withbeetravel.domain.Travel; + +import java.time.LocalDateTime; + +public class SettlementRequestFixture { + + private Long id; + private Travel travel; + private LocalDateTime requestStartTime = LocalDateTime.now(); + private LocalDateTime requestEndTime; + private int disagreeCount = 0; + + public static SettlementRequestFixture builder() { + return new SettlementRequestFixture(); + } + + public SettlementRequestFixture id(Long id) { + this.id = id; + return this; + } + + public SettlementRequestFixture travel(Travel travel) { + this.travel = travel; + return this; + } + + public SettlementRequestFixture requestStartTime(LocalDateTime requestStartTime) { + this.requestStartTime = requestStartTime; + return this; + } + + public SettlementRequestFixture requestEndTime(LocalDateTime requestEndTime) { + this.requestEndTime = requestEndTime; + return this; + } + + public SettlementRequestFixture disagreeCount(int disagreeCount) { + this.disagreeCount = disagreeCount; + return this; + } + + public SettlementRequest build() { + return SettlementRequest.builder() + .id(id) + .travel(travel) + .requestStartTime(requestStartTime) + .requestEndTime(requestEndTime) + .disagreeCount(disagreeCount) + .build(); + } +} diff --git a/src/test/java/withbeetravel/support/SettlementRequestLogFixture.java b/src/test/java/withbeetravel/support/SettlementRequestLogFixture.java new file mode 100644 index 00000000..cff02a28 --- /dev/null +++ b/src/test/java/withbeetravel/support/SettlementRequestLogFixture.java @@ -0,0 +1,70 @@ +package withbeetravel.support; + +import withbeetravel.domain.LogTitle; +import withbeetravel.domain.SettlementRequestLog; +import withbeetravel.domain.Travel; +import withbeetravel.domain.User; + +import java.time.LocalDateTime; + +public class SettlementRequestLogFixture { + + private Long id; + private Travel travel; + private User user; + private LogTitle logTitle = LogTitle.SETTLEMENT_REQUEST; + private String logMessage = "์ •์‚ฐ ์š”์ฒญ"; + private LocalDateTime logTime = LocalDateTime.now(); + private String link = "travel/1/settlement"; + + public static SettlementRequestLogFixture builder() { + return new SettlementRequestLogFixture(); + } + + public SettlementRequestLogFixture id(Long id) { + this.id = id; + return this; + } + + public SettlementRequestLogFixture travel(Travel travel) { + this.travel = travel; + return this; + } + + public SettlementRequestLogFixture user(User user) { + this.user = user; + return this; + } + + public SettlementRequestLogFixture logTitle(LogTitle logTitle) { + this.logTitle = logTitle; + return this; + } + + public SettlementRequestLogFixture logMessage(String logMessage) { + this.logMessage = logMessage; + return this; + } + + public SettlementRequestLogFixture logTime(LocalDateTime logTime) { + this.logTime = logTime; + return this; + } + + public SettlementRequestLogFixture link(String link) { + this.link = link; + return this; + } + + public SettlementRequestLog build() { + return SettlementRequestLog.builder() + .id(id) + .travel(travel) + .user(user) + .logTitle(logTitle) + .logMessage(logMessage) + .logTime(logTime) + .link(link) + .build(); + } +} diff --git a/src/test/java/withbeetravel/support/SettlementTestFixture.java b/src/test/java/withbeetravel/support/SettlementTestFixture.java new file mode 100644 index 00000000..43367056 --- /dev/null +++ b/src/test/java/withbeetravel/support/SettlementTestFixture.java @@ -0,0 +1,73 @@ +package withbeetravel.support; + +import withbeetravel.domain.*; + +public class SettlementTestFixture { + + public static TestData createTestData() { + // User ์ƒ์„ฑ + User user1 = UserFixture.builder().id(1L).build(); + User user2 = UserFixture.builder().id(2L).build(); + + // Account ์ƒ์„ฑ + Account account1 = AccountFixture.builder().user(user1).isConnectedWibeeCard(true).build(); + Account account2 = AccountFixture.builder().user(user2).isConnectedWibeeCard(false).build(); + + user1.updateConnectedAccount(account1); + user1.updateWibeeCardAccount(account1); + user2.updateConnectedAccount(account2); + + // Travel ๋ฐ TravelMember ์ƒ์„ฑ + Travel travel = TravelFixture.builder().id(1L).build(); + TravelMember travelMember1 = TravelMember.builder().id(1L).travel(travel).user(user1).isCaptain(true).build(); + TravelMember travelMember2 = TravelMember.builder().id(2L).travel(travel).user(user2).isCaptain(false).build(); + + // SharedPayment ์ƒ์„ฑ + SharedPayment sharedPayment1 = SharedPaymentFixture.builder() + .id(1L).addedByMember(travelMember1).travel(travel).paymentAmount(70000).build(); + SharedPayment sharedPayment2 = SharedPaymentFixture.builder() + .id(2L).addedByMember(travelMember2).travel(travel).paymentAmount(40000).build(); + + // SettlementRequest ๋ฐ Log ์ƒ์„ฑ + SettlementRequest settlementRequest = SettlementRequestFixture.builder().id(1L).travel(travel).disagreeCount(2).build(); + SettlementRequestLog settlementRequestLog1 = SettlementRequestLogFixture.builder().id(1L).travel(travel).user(user1).build(); + SettlementRequestLog settlementRequestLog2 = SettlementRequestLogFixture.builder().id(2L).travel(travel).user(user2).build(); + + // PaymentParticipatedMember ์ƒ์„ฑ + PaymentParticipatedMember pppm1 = PaymentParticipatedMember.builder().id(1L) + .travelMember(travelMember1).sharedPayment(sharedPayment1).build(); + PaymentParticipatedMember pppm2 = PaymentParticipatedMember.builder().id(2L) + .travelMember(travelMember1).sharedPayment(sharedPayment2).build(); + PaymentParticipatedMember pppm3 = PaymentParticipatedMember.builder().id(3L) + .travelMember(travelMember2).sharedPayment(sharedPayment1).build(); + PaymentParticipatedMember pppm4 = PaymentParticipatedMember.builder().id(4L) + .travelMember(travelMember2).sharedPayment(sharedPayment2).build(); + + // TravelMemberSettlementHistory ์ƒ์„ฑ + TravelMemberSettlementHistory settlementHistory1 = TravelMemberSettlementHistory.builder() + .id(1L) + .settlementRequest(settlementRequest) + .travelMember(travelMember1) + .ownPaymentCost(70000) + .actualBurdenCost(55000) + .isAgreed(false) + .build(); + + TravelMemberSettlementHistory settlementHistory2 = TravelMemberSettlementHistory.builder() + .id(2L) + .settlementRequest(settlementRequest) + .travelMember(travelMember2) + .ownPaymentCost(40000) + .actualBurdenCost(55000) + .isAgreed(false) + .build(); + + // TestData ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐ ๋ฐ˜ํ™˜ + return new TestData( + user1, user2, account1, account2, travel, travelMember1, travelMember2, + sharedPayment1, sharedPayment2, settlementRequest, + settlementRequestLog1, settlementRequestLog2, pppm1, pppm2, pppm3, pppm4, + settlementHistory1, settlementHistory2 + ); + } +} diff --git a/src/test/java/withbeetravel/support/SharedPaymentFixture.java b/src/test/java/withbeetravel/support/SharedPaymentFixture.java new file mode 100644 index 00000000..a010fb77 --- /dev/null +++ b/src/test/java/withbeetravel/support/SharedPaymentFixture.java @@ -0,0 +1,116 @@ +package withbeetravel.support; + +import withbeetravel.domain.*; + +import java.time.LocalDateTime; + +public class SharedPaymentFixture { + + private Long id; + private TravelMember addedByMember; + private Travel travel; + private CurrencyUnit currencyUnit = CurrencyUnit.KRW; + private int paymentAmount = 100000; + private Double foreignPaymentAmount = null; + private Double exchangeRate = null; + private String paymentComment = "๋ง›์žˆ๋Š” ์ €๋…"; + private String paymentImage = null; + private boolean isManuallyAdded = false; + private int participantCount = 2; + private Category category = Category.FOOD; + private String storeName = "์ด์„ ์ƒ ์งœ๊ธ€์ด"; + private LocalDateTime paymentDate = LocalDateTime.now(); + + public static SharedPaymentFixture builder() { + return new SharedPaymentFixture(); + } + + public SharedPaymentFixture id(Long id) { + this.id = id; + return this; + } + + public SharedPaymentFixture addedByMember(TravelMember addedByMember) { + this.addedByMember = addedByMember; + return this; + } + + public SharedPaymentFixture travel(Travel travel) { + this.travel = travel; + return this; + } + + public SharedPaymentFixture currencyUnit(CurrencyUnit currencyUnit) { + this.currencyUnit = currencyUnit; + return this; + } + + public SharedPaymentFixture paymentAmount(int paymentAmount) { + this.paymentAmount = paymentAmount; + return this; + } + + public SharedPaymentFixture foreignPaymentAmount(Double foreignPaymentAmount) { + this.foreignPaymentAmount = foreignPaymentAmount; + return this; + } + + public SharedPaymentFixture exchangeRate(Double exchangeRate) { + this.exchangeRate = exchangeRate; + return this; + } + + public SharedPaymentFixture paymentComment(String paymentComment) { + this.paymentComment = paymentComment; + return this; + } + + public SharedPaymentFixture paymentImage(String paymentImage) { + this.paymentImage = paymentImage; + return this; + } + + public SharedPaymentFixture isManuallyAdded(boolean isManuallyAdded) { + this.isManuallyAdded = isManuallyAdded; + return this; + } + + public SharedPaymentFixture participantCount(int participantCount) { + this.participantCount = participantCount; + return this; + } + + public SharedPaymentFixture category(Category category) { + this.category = category; + return this; + } + + public SharedPaymentFixture storeName(String storeName) { + this.storeName = storeName; + return this; + } + + public SharedPaymentFixture paymentDate(LocalDateTime paymentDate) { + this.paymentDate = paymentDate; + return this; + } + + public SharedPayment build() { + return SharedPayment.builder() + .id(id) + .addedByMember(addedByMember) + .travel(travel) + .currencyUnit(currencyUnit) + .paymentAmount(paymentAmount) + .foreignPaymentAmount(foreignPaymentAmount) + .exchangeRate(exchangeRate) + .paymentComment(paymentComment) + .paymentImage(paymentImage) + .isManuallyAdded(isManuallyAdded) + .participantCount(participantCount) + .category(category) + .storeName(storeName) + .paymentDate(paymentDate) + .build(); + } +} diff --git a/src/test/java/withbeetravel/support/TestData.java b/src/test/java/withbeetravel/support/TestData.java new file mode 100644 index 00000000..1ffcf24c --- /dev/null +++ b/src/test/java/withbeetravel/support/TestData.java @@ -0,0 +1,53 @@ +package withbeetravel.support; + +import withbeetravel.domain.*; + +public class TestData { + public final User user1; + public final User user2; + public final Account account1; + public final Account account2; + public final Travel travel; + public final TravelMember travelMember1; + public final TravelMember travelMember2; + public final SharedPayment sharedPayment1; + public final SharedPayment sharedPayment2; + public final SettlementRequest settlementRequest; + public final SettlementRequestLog settlementRequestLog1; + public final SettlementRequestLog settlementRequestLog2; + public final PaymentParticipatedMember pppm1; + public final PaymentParticipatedMember pppm2; + public final PaymentParticipatedMember pppm3; + public final PaymentParticipatedMember pppm4; + public final TravelMemberSettlementHistory settlementHistory1; + public final TravelMemberSettlementHistory settlementHistory2; + + public TestData( + User user1, User user2, Account account1, Account account2, + Travel travel, TravelMember travelMember1, TravelMember travelMember2, + SharedPayment sharedPayment1, SharedPayment sharedPayment2, + SettlementRequest settlementRequest, SettlementRequestLog settlementRequestLog1, + SettlementRequestLog settlementRequestLog2, PaymentParticipatedMember pppm1, + PaymentParticipatedMember pppm2, PaymentParticipatedMember pppm3, + PaymentParticipatedMember pppm4, TravelMemberSettlementHistory settlementHistory1, + TravelMemberSettlementHistory settlementHistory2) { + this.user1 = user1; + this.user2 = user2; + this.account1 = account1; + this.account2 = account2; + this.travel = travel; + this.travelMember1 = travelMember1; + this.travelMember2 = travelMember2; + this.sharedPayment1 = sharedPayment1; + this.sharedPayment2 = sharedPayment2; + this.settlementRequest = settlementRequest; + this.settlementRequestLog1 = settlementRequestLog1; + this.settlementRequestLog2 = settlementRequestLog2; + this.pppm1 = pppm1; + this.pppm2 = pppm2; + this.pppm3 = pppm3; + this.pppm4 = pppm4; + this.settlementHistory1 = settlementHistory1; + this.settlementHistory2 = settlementHistory2; + } +} diff --git a/src/test/java/withbeetravel/support/TravelFixture.java b/src/test/java/withbeetravel/support/TravelFixture.java new file mode 100644 index 00000000..06675772 --- /dev/null +++ b/src/test/java/withbeetravel/support/TravelFixture.java @@ -0,0 +1,75 @@ +package withbeetravel.support; + +import withbeetravel.domain.SettlementStatus; +import withbeetravel.domain.Travel; + +import java.time.LocalDate; + +public class TravelFixture { + + private Long id; + private String travelName = "๊ฐ•๋ฆ‰ ์—ฌํ–‰"; + private LocalDate travelStartDate = LocalDate.now(); + private LocalDate travelEndDate = LocalDate.now().plusDays(4); + private String inviteCode = "INV123456"; + private String mainImage = null; + private boolean isDomesticTravel = true; + private SettlementStatus settlementStatus = SettlementStatus.PENDING; + + public static TravelFixture builder() { + return new TravelFixture(); + } + + public TravelFixture id(Long id) { + this.id = id; + return this; + } + + public TravelFixture travelName(String travelName) { + this.travelName = travelName; + return this; + } + + public TravelFixture travelStartDate(LocalDate travelStartDate) { + this.travelStartDate = travelStartDate; + return this; + } + + public TravelFixture travelEndDate(LocalDate travelEndDate) { + this.travelEndDate = travelEndDate; + return this; + } + + public TravelFixture inviteCode(String inviteCode) { + this.inviteCode = inviteCode; + return this; + } + + public TravelFixture mainImage(String mainImage) { + this.mainImage = mainImage; + return this; + } + + public TravelFixture isDomesticTravel(boolean isDomesticTravel) { + this.isDomesticTravel = isDomesticTravel; + return this; + } + + public TravelFixture settlementStatus(SettlementStatus settlementStatus) { + this.settlementStatus = settlementStatus; + return this; + } + + public Travel build() { + return Travel.builder() + .id(id) + .travelName(travelName) + .travelStartDate(travelStartDate) + .travelEndDate(travelEndDate) + .inviteCode(inviteCode) + .mainImage(mainImage) + .isDomesticTravel(isDomesticTravel) + .settlementStatus(settlementStatus) + .build(); + } +} diff --git a/src/test/java/withbeetravel/support/UserFixture.java b/src/test/java/withbeetravel/support/UserFixture.java new file mode 100644 index 00000000..09373b17 --- /dev/null +++ b/src/test/java/withbeetravel/support/UserFixture.java @@ -0,0 +1,97 @@ +package withbeetravel.support; + +import withbeetravel.domain.Account; +import withbeetravel.domain.RoleType; +import withbeetravel.domain.User; + +public class UserFixture { + + private Long id; + private Account wibeeCardAccount; + private Account connectedAccount; + private String email = "1234@naver.com"; + private String password = "password123!"; + private String pinNumber = "123456"; + private String name = "ํ™๊ธธ๋™"; + private int profileImage = 1; + private int failedPinCount = 0; + private boolean pinLocked = false; + private RoleType roleType = RoleType.USER; + + public static UserFixture builder() { + return new UserFixture(); + } + + public UserFixture id(Long id) { + this.id = id; + return this; + } + + public UserFixture wibeeCardAccount(Account wibeeCardAccount) { + this.wibeeCardAccount = wibeeCardAccount; + return this; + } + + public UserFixture connectedAccount(Account connectedAccount) { + this.connectedAccount = connectedAccount; + return this; + } + + public UserFixture email(String email) { + this.email = email; + return this; + } + + public UserFixture password(String password) { + this.password = password; + return this; + } + + public UserFixture pinNumber(String pinNumber) { + this.pinNumber = pinNumber; + return this; + } + + public UserFixture name(String name) { + this.name = name; + return this; + } + + public UserFixture profileImage(int profileImage) { + this.profileImage = profileImage; + return this; + } + + public UserFixture failedPinCount(int failedPinCount) { + this.failedPinCount = failedPinCount; + return this; + } + + public UserFixture pinLocked(boolean pinLocked) { + this.pinLocked = pinLocked; + return this; + } + + public UserFixture roleType(RoleType roleType) { + this.roleType = roleType; + return this; + } + + public User build() { + return User.builder() + .id(id) + .wibeeCardAccount(wibeeCardAccount) + .connectedAccount(connectedAccount) + .email(email) + .password(password) + .pinNumber(pinNumber) + .name(name) + .profileImage(profileImage) + .failedPinCount(failedPinCount) + .pinLocked(pinLocked) + .roleType(roleType) + .build(); + } + + +}